VPP load-balancer dataplane integration: state, sync, and global conf
This commit wires maglevd through to VPP's LB plugin end-to-end, using
locally-generated GoVPP bindings for the newer v2 API messages.
VPP binapi (vendored)
- New package internal/vpp/binapi/ containing lb, lb_types, ip_types, and
interface_types, generated from a local VPP build (~/src/vpp) via a new
'make vpp-binapi' target. GoVPP v0.12.0 upstream lacks the v2 messages we
need (lb_conf_get, lb_add_del_vip_v2, lb_add_del_as_v2, lb_as_v2_dump,
lb_as_set_weight), so we commit the generated output in-tree.
- All generated files go through our loggedChannel wrapper; every VPP API
send/receive is recorded at DEBUG via slog (vpp-api-send / vpp-api-recv /
vpp-api-send-multi / vpp-api-recv-multi) so the full wire-level trail is
auditable. NewAPIChannel is unexported — callers must use c.apiChannel().
Read path: GetLBState{All,VIP}
- GetLBStateAll returns a full snapshot (global conf + every VIP with its
attached application servers).
- GetLBStateVIP looks up a single VIP by (prefix, protocol, port) and
returns (nil, nil) when the VIP doesn't exist in VPP. This is the
efficient path for targeted updates on a busy LB.
- Helpers factored out: getLBConf, dumpAllVIPs, dumpASesForVIP, lookupVIP,
vipFromDetails.
Write path: SyncLBState{All,VIP}
- SyncLBStateAll reconciles every configured frontend with VPP: creates
missing VIPs, removes stale ones (with AS flush), and reconciles AS
membership and weights within VIPs that exist on both sides.
- SyncLBStateVIP targets a single frontend by name. Never removes VIPs.
Returns ErrFrontendNotFound (wrapped with the name) when the frontend
isn't in config, so callers can use errors.Is.
- Shared reconcileVIP helper does the per-VIP AS diff; removeVIP is used
only by the full-sync pass.
- LbAddDelVipV2 requests always set NewFlowsTableLength=1024. The .api
default=1024 annotation is only applied by VAT/CLI parsers, not wire-
level marshalling — sending 0 caused VPP to vec_validate with mask
0xFFFFFFFF and OOM-panic.
- Pool semantics: backends in the primary (first) pool of a frontend get
their configured weight; backends in secondary pools get weight 0. All
backends are installed so higher layers can flip weights on failover
without add/remove churn.
- Every individual change emits a DEBUG slog (vpp-lbsync-vip-add/del,
vpp-lbsync-as-add/del, vpp-lbsync-as-weight). Start/done INFO logs
carry a scope=all|vip label plus aggregate counts.
Global conf push: SetLBConf
- New SetLBConf(cfg) sends lb_conf with ipv4-src, ipv6-src, sticky-buckets,
and flow-timeout. Called automatically on VPP (re)connect and after
every config reload (via doReloadConfig). Results are cached on the
Client so redundant pushes are silently skipped — only actual changes
produce a vpp-lb-conf-set INFO log line.
Periodic drift reconciliation
- vpp.Client.lbSyncLoop runs in a goroutine tied to each VPP connection's
lifetime. Its first tick is immediate (startup and post-reconnect
sync quickly); subsequent ticks fire every vpp.lb.sync-interval from
config (default 30s). Purpose: catch drift if something/someone
modifies VPP state by hand. The loop uses a ConfigSource interface
(satisfied by checker.Checker via its new Config() accessor) to avoid
an import cycle with the checker package.
Config schema additions (maglev.vpp.lb)
- sync-interval: positive Go duration, default 30s.
- ipv4-src-address: REQUIRED. Used as the outer source for GRE4 encap
to application servers. Missing this is a hard semantic error —
maglevd --check exits 2 and the daemon refuses to start. VPP GRE
needs a source address and every VIP we program uses GRE, so there
is no meaningful config without it.
- ipv6-src-address: REQUIRED. Same treatment as ipv4-src-address.
- sticky-buckets-per-core: default 65536, must be a power of 2.
- flow-timeout: default 40s, must be a whole number of seconds in [1s, 120s].
- VPP validation runs at the end of convert() so structural errors in
healthchecks/backends/frontends surface first — operators fix those,
then get the VPP-specific requirements.
gRPC API
- New GetVPPLBState RPC returning VPPLBState: global conf + VIPs with
ASes. Mirrors the read-path but strips fields irrelevant to our
GRE-only deployment (srv_type, dscp, target_port).
- New SyncVPPLBState RPC with optional frontend_name. Unset → full sync
(may remove stale VIPs). Set → single-VIP sync (never removes).
Returns codes.NotFound for unknown frontends, codes.Unavailable when
VPP integration is disabled or disconnected.
maglevc (CLI)
- New 'show vpp lbstate' command displaying the LB plugin state. VPP-only
fields the dataplane irrelevant to GRE are suppressed. Per-AS lines use
a key-value format ("address X weight Y flow-table-buckets Z")
instead of a tabwriter column, which avoids the ANSI-color alignment
issue we hit with mixed label/data rows.
- New 'sync vpp lbstate [<name>]' command. Without a name, triggers a
full reconciliation; with a name, targets one frontend.
- Previous 'show vpp lb' renamed to 'show vpp lbstate' for consistency
with the new sync command.
Test fixtures
- validConfig and all ad-hoc config_test.go fixtures that reach the end
of convert() now include the two required vpp.lb src addresses.
- tests/01-maglevd/maglevd-lab/maglev.yaml gains a vpp.lb section so the
robot integration tests can still load the config.
- cmd/maglevc/tree_test.go gains expected paths for the new commands.
Docs
- config-guide.md: new 'vpp' section in the basic structure, detailed
vpp.lb field reference, noting ipv4/ipv6 src addresses as REQUIRED
(hard error) with no defaults; example config updated.
- user-guide.md: documented 'show vpp info', 'show vpp lbstate',
'sync vpp lbstate [<name>]', new --vpp-api-addr and --vpp-stats-addr
flags, the vpp-lb-conf-set log line, and corrected the pause/resume
description to reflect that pause cancels the probe goroutine.
- debian/maglev.yaml: example config gains a vpp.lb block with src
addresses and commented optional overrides.
This commit is contained in:
25
Makefile
25
Makefile
@@ -14,7 +14,9 @@ LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
|
|||||||
|
|
||||||
TEST ?= tests/
|
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
|
all: build
|
||||||
|
|
||||||
@@ -48,6 +50,27 @@ $(GEN_FILES): $(PROTO_FILE)
|
|||||||
--go-grpc_out=. --go-grpc_opt=module=$(MODULE) \
|
--go-grpc_out=. --go-grpc_opt=module=$(MODULE) \
|
||||||
$(PROTO_FILE)
|
$(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:
|
fixstyle:
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
|
|
||||||
|
|||||||
@@ -71,12 +71,13 @@ func buildTree() *Node {
|
|||||||
Children: []*Node{showHealthCheckName},
|
Children: []*Node{showHealthCheckName},
|
||||||
}
|
}
|
||||||
|
|
||||||
// show vpp info
|
// show vpp info / lbstate
|
||||||
showVPPInfo := &Node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo}
|
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{
|
showVPP := &Node{
|
||||||
Word: "vpp",
|
Word: "vpp",
|
||||||
Help: "VPP dataplane information",
|
Help: "VPP dataplane information",
|
||||||
Children: []*Node{showVPPInfo},
|
Children: []*Node{showVPPInfo, showVPPLBState},
|
||||||
}
|
}
|
||||||
|
|
||||||
show.Children = []*Node{
|
show.Children = []*Node{
|
||||||
@@ -174,7 +175,34 @@ func buildTree() *Node {
|
|||||||
Children: []*Node{configCheck, configReload},
|
Children: []*Node{configCheck, configReload},
|
||||||
}
|
}
|
||||||
|
|
||||||
root.Children = []*Node{show, set, watch, configNode, quit, exit}
|
// sync vpp lbstate [<name>]
|
||||||
|
//
|
||||||
|
// Without a name: run SyncLBStateAll (may remove stale VIPs).
|
||||||
|
// With a name: run SyncLBStateVIP(name) for just that frontend (no removals).
|
||||||
|
syncVPPLBStateName := &Node{
|
||||||
|
Word: "<name>",
|
||||||
|
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
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +263,89 @@ func runShowVPPInfo(ctx context.Context, client grpcapi.MaglevClient, _ []string
|
|||||||
return w.Flush()
|
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 {
|
func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
|
||||||
fmt.Printf("maglevc %s (commit %s, built %s)\n",
|
fmt.Printf("maglevc %s (commit %s, built %s)\n",
|
||||||
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ func TestExpandPathsRoot(t *testing.T) {
|
|||||||
"watch events <opt>",
|
"watch events <opt>",
|
||||||
"config check",
|
"config check",
|
||||||
"show vpp info",
|
"show vpp info",
|
||||||
|
"show vpp lbstate",
|
||||||
"config reload",
|
"config reload",
|
||||||
|
"sync vpp lbstate",
|
||||||
|
"sync vpp lbstate <name>",
|
||||||
"quit",
|
"quit",
|
||||||
"exit",
|
"exit",
|
||||||
}
|
}
|
||||||
@@ -60,9 +63,9 @@ func TestExpandPathsShow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// version, frontends, frontends <name>, backends, backends <name>,
|
// version, frontends, frontends <name>, backends, backends <name>,
|
||||||
// healthchecks, healthchecks <name>, vpp info = 8 lines
|
// healthchecks, healthchecks <name>, vpp info, vpp lb = 9 lines
|
||||||
if len(lines) != 8 {
|
if len(lines) != 9 {
|
||||||
t.Errorf("expected exactly 8 show subcommands, got %d", len(lines))
|
t.Errorf("expected exactly 9 show subcommands, got %d", len(lines))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func run() error {
|
|||||||
var vppClient *vpp.Client
|
var vppClient *vpp.Client
|
||||||
if *vppAPIAddr != "" {
|
if *vppAPIAddr != "" {
|
||||||
vppClient = vpp.New(*vppAPIAddr, *vppStatsAddr)
|
vppClient = vpp.New(*vppAPIAddr, *vppStatsAddr)
|
||||||
|
vppClient.SetConfigSource(chkr)
|
||||||
go vppClient.Run(ctx)
|
go vppClient.Run(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
debian/maglev.yaml
vendored
7
debian/maglev.yaml
vendored
@@ -3,6 +3,13 @@ maglev:
|
|||||||
transition-history: 5
|
transition-history: 5
|
||||||
# netns: dataplane # run probes inside a named network namespace
|
# 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:
|
healthchecks:
|
||||||
http-check:
|
http-check:
|
||||||
type: http
|
type: http
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ in two stages:
|
|||||||
ensuring that every backend referenced by a frontend exists, that address families are
|
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.
|
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
|
## Basic structure
|
||||||
|
|
||||||
@@ -22,6 +22,10 @@ maglev:
|
|||||||
healthchecker:
|
healthchecker:
|
||||||
[ Global health checker settings ]
|
[ Global health checker settings ]
|
||||||
|
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
[ VPP load-balancer integration settings ]
|
||||||
|
|
||||||
healthchecks:
|
healthchecks:
|
||||||
my-check:
|
my-check:
|
||||||
[ Health check definition ]
|
[ Health check definition ]
|
||||||
@@ -35,9 +39,11 @@ maglev:
|
|||||||
[ Frontend (VIP) definition ]
|
[ 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
|
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
|
## healthchecks
|
||||||
|
|
||||||
A named map of health check definitions. Each health check describes *how* to probe a backend.
|
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,
|
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 [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 the [user-guide.md](user-guide.md).
|
||||||
|
|
||||||
|
|||||||
@@ -88,10 +88,26 @@ show healthchecks [<name>] Without name: list all health-check names.
|
|||||||
show vpp info Show VPP version, build date, PID, uptime, and when
|
show vpp info Show VPP version, build date, PID, uptime, and when
|
||||||
maglevd connected. Returns an error if VPP is not
|
maglevd connected. Returns an error if VPP is not
|
||||||
connected.
|
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 <name> pause Suspend health checking for a backend, freezing its state.
|
sync vpp lbstate [<name>] Reconcile the VPP load-balancer dataplane from the
|
||||||
set backend <name> resume Resume health checking; backend re-enters unknown state
|
running config. Without a name: runs a full sync —
|
||||||
and is probed immediately.
|
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 <name> 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 <name> resume Resume health checking. A fresh probe goroutine is
|
||||||
|
started and the backend re-enters unknown state.
|
||||||
set backend <name> disable Stop probing entirely and remove the backend from rotation.
|
set backend <name> disable Stop probing entirely and remove the backend from rotation.
|
||||||
The backend remains visible (state: disabled) and can be
|
The backend remains visible (state: disabled) and can be
|
||||||
re-enabled without reloading configuration.
|
re-enabled without reloading configuration.
|
||||||
|
|||||||
@@ -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.
|
// ListFrontends returns the names of all configured frontends.
|
||||||
func (c *Checker) ListFrontends() []string {
|
func (c *Checker) ListFrontends() []string {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
// Config is the top-level parsed and validated configuration.
|
// Config is the top-level parsed and validated configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HealthChecker HealthCheckerConfig
|
HealthChecker HealthCheckerConfig
|
||||||
|
VPP VPPConfig
|
||||||
HealthChecks map[string]HealthCheck
|
HealthChecks map[string]HealthCheck
|
||||||
Backends map[string]Backend
|
Backends map[string]Backend
|
||||||
Frontends map[string]Frontend
|
Frontends map[string]Frontend
|
||||||
@@ -28,6 +29,37 @@ type HealthCheckerConfig struct {
|
|||||||
Netns string // network namespace for probes; "" = current netns
|
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.
|
// HealthCheck describes how to probe a backend.
|
||||||
type HealthCheck struct {
|
type HealthCheck struct {
|
||||||
Type string
|
Type string
|
||||||
@@ -97,6 +129,7 @@ type rawConfig struct {
|
|||||||
|
|
||||||
type rawMaglev struct {
|
type rawMaglev struct {
|
||||||
HealthChecker rawHealthCheckerCfg `yaml:"healthchecker"`
|
HealthChecker rawHealthCheckerCfg `yaml:"healthchecker"`
|
||||||
|
VPP rawVPPCfg `yaml:"vpp"`
|
||||||
HealthChecks map[string]rawHealthCheck `yaml:"healthchecks"`
|
HealthChecks map[string]rawHealthCheck `yaml:"healthchecks"`
|
||||||
Backends map[string]rawBackend `yaml:"backends"`
|
Backends map[string]rawBackend `yaml:"backends"`
|
||||||
Frontends map[string]rawFrontend `yaml:"frontends"`
|
Frontends map[string]rawFrontend `yaml:"frontends"`
|
||||||
@@ -107,6 +140,18 @@ type rawHealthCheckerCfg struct {
|
|||||||
Netns string `yaml:"netns"`
|
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 rawHealthCheck struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Port uint16 `yaml:"port"`
|
Port uint16 `yaml:"port"`
|
||||||
@@ -255,9 +300,93 @@ func convert(r *rawMaglev) (*Config, error) {
|
|||||||
cfg.Frontends[name] = fe
|
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
|
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) {
|
func convertHealthCheck(r *rawHealthCheck) (HealthCheck, error) {
|
||||||
h := HealthCheck{Type: r.Type, Port: r.Port}
|
h := HealthCheck{Type: r.Type, Port: r.Port}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ maglev:
|
|||||||
healthchecker:
|
healthchecker:
|
||||||
transition-history: 5
|
transition-history: 5
|
||||||
netns: dataplane
|
netns: dataplane
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
healthchecks:
|
healthchecks:
|
||||||
http-check:
|
http-check:
|
||||||
type: http
|
type: http
|
||||||
@@ -150,6 +154,10 @@ func TestValidConfig(t *testing.T) {
|
|||||||
func TestDefaults(t *testing.T) {
|
func TestDefaults(t *testing.T) {
|
||||||
raw := `
|
raw := `
|
||||||
maglev:
|
maglev:
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
healthchecks:
|
healthchecks:
|
||||||
icmp:
|
icmp:
|
||||||
type: icmp
|
type: icmp
|
||||||
@@ -196,6 +204,10 @@ func TestBackendNoHealthcheck(t *testing.T) {
|
|||||||
// A backend with no healthcheck reference is valid; probe is skipped.
|
// A backend with no healthcheck reference is valid; probe is skipped.
|
||||||
raw := `
|
raw := `
|
||||||
maglev:
|
maglev:
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
healthchecks: {}
|
healthchecks: {}
|
||||||
backends:
|
backends:
|
||||||
be:
|
be:
|
||||||
@@ -220,6 +232,10 @@ maglev:
|
|||||||
func TestOptionalIntervals(t *testing.T) {
|
func TestOptionalIntervals(t *testing.T) {
|
||||||
raw := `
|
raw := `
|
||||||
maglev:
|
maglev:
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
healthchecks:
|
healthchecks:
|
||||||
icmp:
|
icmp:
|
||||||
type: icmp
|
type: icmp
|
||||||
@@ -259,6 +275,10 @@ func TestValidationErrors(t *testing.T) {
|
|||||||
base := func(hcExtra, beExtra, feExtra string) string {
|
base := func(hcExtra, beExtra, feExtra string) string {
|
||||||
return `
|
return `
|
||||||
maglev:
|
maglev:
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
healthchecks:
|
healthchecks:
|
||||||
c:
|
c:
|
||||||
type: icmp
|
type: icmp
|
||||||
@@ -294,6 +314,10 @@ maglev:
|
|||||||
name: "mixed backend address families in pool",
|
name: "mixed backend address families in pool",
|
||||||
yaml: `
|
yaml: `
|
||||||
maglev:
|
maglev:
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
healthchecks:
|
healthchecks:
|
||||||
c:
|
c:
|
||||||
type: icmp
|
type: icmp
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,8 @@ const (
|
|||||||
Maglev_CheckConfig_FullMethodName = "/maglev.Maglev/CheckConfig"
|
Maglev_CheckConfig_FullMethodName = "/maglev.Maglev/CheckConfig"
|
||||||
Maglev_ReloadConfig_FullMethodName = "/maglev.Maglev/ReloadConfig"
|
Maglev_ReloadConfig_FullMethodName = "/maglev.Maglev/ReloadConfig"
|
||||||
Maglev_GetVPPInfo_FullMethodName = "/maglev.Maglev/GetVPPInfo"
|
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.
|
// 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)
|
CheckConfig(ctx context.Context, in *CheckConfigRequest, opts ...grpc.CallOption) (*CheckConfigResponse, error)
|
||||||
ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error)
|
ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error)
|
||||||
GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, opts ...grpc.CallOption) (*VPPInfo, 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 {
|
type maglevClient struct {
|
||||||
@@ -226,6 +230,26 @@ func (c *maglevClient) GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, op
|
|||||||
return out, nil
|
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.
|
// MaglevServer is the server API for Maglev service.
|
||||||
// All implementations must embed UnimplementedMaglevServer
|
// All implementations must embed UnimplementedMaglevServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -247,6 +271,8 @@ type MaglevServer interface {
|
|||||||
CheckConfig(context.Context, *CheckConfigRequest) (*CheckConfigResponse, error)
|
CheckConfig(context.Context, *CheckConfigRequest) (*CheckConfigResponse, error)
|
||||||
ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error)
|
ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error)
|
||||||
GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error)
|
GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error)
|
||||||
|
GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error)
|
||||||
|
SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error)
|
||||||
mustEmbedUnimplementedMaglevServer()
|
mustEmbedUnimplementedMaglevServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +328,12 @@ func (UnimplementedMaglevServer) ReloadConfig(context.Context, *ReloadConfigRequ
|
|||||||
func (UnimplementedMaglevServer) GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error) {
|
func (UnimplementedMaglevServer) GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetVPPInfo not implemented")
|
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) mustEmbedUnimplementedMaglevServer() {}
|
||||||
func (UnimplementedMaglevServer) testEmbeddedByValue() {}
|
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)
|
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.
|
// Maglev_ServiceDesc is the grpc.ServiceDesc for Maglev service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -649,6 +717,14 @@ var Maglev_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "GetVPPInfo",
|
MethodName: "GetVPPInfo",
|
||||||
Handler: _Maglev_GetVPPInfo_Handler,
|
Handler: _Maglev_GetVPPInfo_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetVPPLBState",
|
||||||
|
Handler: _Maglev_GetVPPLBState_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SyncVPPLBState",
|
||||||
|
Handler: _Maglev_SyncVPPLBState_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package grpcapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
@@ -255,6 +256,13 @@ func (s *Server) doReloadConfig() *ReloadConfigResponse {
|
|||||||
ReloadError: err.Error(),
|
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))
|
slog.Info("config-reload-done", "frontends", len(newCfg.Frontends))
|
||||||
return &ReloadConfigResponse{Ok: true}
|
return &ReloadConfigResponse{Ok: true}
|
||||||
}
|
}
|
||||||
@@ -282,6 +290,84 @@ func (s *Server) GetVPPInfo(_ context.Context, _ *GetVPPInfoRequest) (*VPPInfo,
|
|||||||
}, nil
|
}, 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 ----------------------------------------------------
|
// ---- conversion helpers ----------------------------------------------------
|
||||||
|
|
||||||
func frontendToProto(name string, fe config.Frontend) *FrontendInfo {
|
func frontendToProto(name string, fe config.Frontend) *FrontendInfo {
|
||||||
|
|||||||
122
internal/vpp/apilog.go
Normal file
122
internal/vpp/apilog.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
304
internal/vpp/binapi/interface_types/interface_types.ba.go
Normal file
304
internal/vpp/binapi/interface_types/interface_types.ba.go
Normal file
@@ -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<<i) != 0 {
|
||||||
|
if s != "" {
|
||||||
|
s += "|"
|
||||||
|
}
|
||||||
|
s += str(1 << i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
return str(uint32(x))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// IfType defines enum 'if_type'.
|
||||||
|
type IfType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
IF_API_TYPE_HARDWARE IfType = 0
|
||||||
|
IF_API_TYPE_SUB IfType = 1
|
||||||
|
IF_API_TYPE_P2P IfType = 2
|
||||||
|
IF_API_TYPE_PIPE IfType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IfType_name = map[uint32]string{
|
||||||
|
0: "IF_API_TYPE_HARDWARE",
|
||||||
|
1: "IF_API_TYPE_SUB",
|
||||||
|
2: "IF_API_TYPE_P2P",
|
||||||
|
3: "IF_API_TYPE_PIPE",
|
||||||
|
}
|
||||||
|
IfType_value = map[string]uint32{
|
||||||
|
"IF_API_TYPE_HARDWARE": 0,
|
||||||
|
"IF_API_TYPE_SUB": 1,
|
||||||
|
"IF_API_TYPE_P2P": 2,
|
||||||
|
"IF_API_TYPE_PIPE": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x IfType) String() string {
|
||||||
|
s, ok := IfType_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "IfType(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkDuplex defines enum 'link_duplex'.
|
||||||
|
type LinkDuplex uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LINK_DUPLEX_API_UNKNOWN LinkDuplex = 0
|
||||||
|
LINK_DUPLEX_API_HALF LinkDuplex = 1
|
||||||
|
LINK_DUPLEX_API_FULL LinkDuplex = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LinkDuplex_name = map[uint32]string{
|
||||||
|
0: "LINK_DUPLEX_API_UNKNOWN",
|
||||||
|
1: "LINK_DUPLEX_API_HALF",
|
||||||
|
2: "LINK_DUPLEX_API_FULL",
|
||||||
|
}
|
||||||
|
LinkDuplex_value = map[string]uint32{
|
||||||
|
"LINK_DUPLEX_API_UNKNOWN": 0,
|
||||||
|
"LINK_DUPLEX_API_HALF": 1,
|
||||||
|
"LINK_DUPLEX_API_FULL": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x LinkDuplex) String() string {
|
||||||
|
s, ok := LinkDuplex_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "LinkDuplex(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MtuProto defines enum 'mtu_proto'.
|
||||||
|
type MtuProto uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
MTU_PROTO_API_L3 MtuProto = 0
|
||||||
|
MTU_PROTO_API_IP4 MtuProto = 1
|
||||||
|
MTU_PROTO_API_IP6 MtuProto = 2
|
||||||
|
MTU_PROTO_API_MPLS MtuProto = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MtuProto_name = map[uint32]string{
|
||||||
|
0: "MTU_PROTO_API_L3",
|
||||||
|
1: "MTU_PROTO_API_IP4",
|
||||||
|
2: "MTU_PROTO_API_IP6",
|
||||||
|
3: "MTU_PROTO_API_MPLS",
|
||||||
|
}
|
||||||
|
MtuProto_value = map[string]uint32{
|
||||||
|
"MTU_PROTO_API_L3": 0,
|
||||||
|
"MTU_PROTO_API_IP4": 1,
|
||||||
|
"MTU_PROTO_API_IP6": 2,
|
||||||
|
"MTU_PROTO_API_MPLS": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x MtuProto) String() string {
|
||||||
|
s, ok := MtuProto_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "MtuProto(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RxMode defines enum 'rx_mode'.
|
||||||
|
type RxMode uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
RX_MODE_API_UNKNOWN RxMode = 0
|
||||||
|
RX_MODE_API_POLLING RxMode = 1
|
||||||
|
RX_MODE_API_INTERRUPT RxMode = 2
|
||||||
|
RX_MODE_API_ADAPTIVE RxMode = 3
|
||||||
|
RX_MODE_API_DEFAULT RxMode = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
RxMode_name = map[uint32]string{
|
||||||
|
0: "RX_MODE_API_UNKNOWN",
|
||||||
|
1: "RX_MODE_API_POLLING",
|
||||||
|
2: "RX_MODE_API_INTERRUPT",
|
||||||
|
3: "RX_MODE_API_ADAPTIVE",
|
||||||
|
4: "RX_MODE_API_DEFAULT",
|
||||||
|
}
|
||||||
|
RxMode_value = map[string]uint32{
|
||||||
|
"RX_MODE_API_UNKNOWN": 0,
|
||||||
|
"RX_MODE_API_POLLING": 1,
|
||||||
|
"RX_MODE_API_INTERRUPT": 2,
|
||||||
|
"RX_MODE_API_ADAPTIVE": 3,
|
||||||
|
"RX_MODE_API_DEFAULT": 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x RxMode) String() string {
|
||||||
|
s, ok := RxMode_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "RxMode(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubIfFlags defines enum 'sub_if_flags'.
|
||||||
|
type SubIfFlags uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
SUB_IF_API_FLAG_NO_TAGS SubIfFlags = 1
|
||||||
|
SUB_IF_API_FLAG_ONE_TAG SubIfFlags = 2
|
||||||
|
SUB_IF_API_FLAG_TWO_TAGS SubIfFlags = 4
|
||||||
|
SUB_IF_API_FLAG_DOT1AD SubIfFlags = 8
|
||||||
|
SUB_IF_API_FLAG_EXACT_MATCH SubIfFlags = 16
|
||||||
|
SUB_IF_API_FLAG_DEFAULT SubIfFlags = 32
|
||||||
|
SUB_IF_API_FLAG_OUTER_VLAN_ID_ANY SubIfFlags = 64
|
||||||
|
SUB_IF_API_FLAG_INNER_VLAN_ID_ANY SubIfFlags = 128
|
||||||
|
SUB_IF_API_FLAG_MASK_VNET SubIfFlags = 254
|
||||||
|
SUB_IF_API_FLAG_DOT1AH SubIfFlags = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
SubIfFlags_name = map[uint32]string{
|
||||||
|
1: "SUB_IF_API_FLAG_NO_TAGS",
|
||||||
|
2: "SUB_IF_API_FLAG_ONE_TAG",
|
||||||
|
4: "SUB_IF_API_FLAG_TWO_TAGS",
|
||||||
|
8: "SUB_IF_API_FLAG_DOT1AD",
|
||||||
|
16: "SUB_IF_API_FLAG_EXACT_MATCH",
|
||||||
|
32: "SUB_IF_API_FLAG_DEFAULT",
|
||||||
|
64: "SUB_IF_API_FLAG_OUTER_VLAN_ID_ANY",
|
||||||
|
128: "SUB_IF_API_FLAG_INNER_VLAN_ID_ANY",
|
||||||
|
254: "SUB_IF_API_FLAG_MASK_VNET",
|
||||||
|
256: "SUB_IF_API_FLAG_DOT1AH",
|
||||||
|
}
|
||||||
|
SubIfFlags_value = map[string]uint32{
|
||||||
|
"SUB_IF_API_FLAG_NO_TAGS": 1,
|
||||||
|
"SUB_IF_API_FLAG_ONE_TAG": 2,
|
||||||
|
"SUB_IF_API_FLAG_TWO_TAGS": 4,
|
||||||
|
"SUB_IF_API_FLAG_DOT1AD": 8,
|
||||||
|
"SUB_IF_API_FLAG_EXACT_MATCH": 16,
|
||||||
|
"SUB_IF_API_FLAG_DEFAULT": 32,
|
||||||
|
"SUB_IF_API_FLAG_OUTER_VLAN_ID_ANY": 64,
|
||||||
|
"SUB_IF_API_FLAG_INNER_VLAN_ID_ANY": 128,
|
||||||
|
"SUB_IF_API_FLAG_MASK_VNET": 254,
|
||||||
|
"SUB_IF_API_FLAG_DOT1AH": 256,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x SubIfFlags) String() string {
|
||||||
|
s, ok := SubIfFlags_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
str := func(n uint32) string {
|
||||||
|
s, ok := SubIfFlags_name[uint32(n)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "SubIfFlags(" + strconv.Itoa(int(n)) + ")"
|
||||||
|
}
|
||||||
|
for i := uint32(0); i <= 32; i++ {
|
||||||
|
val := uint32(x)
|
||||||
|
if val&(1<<i) != 0 {
|
||||||
|
if s != "" {
|
||||||
|
s += "|"
|
||||||
|
}
|
||||||
|
s += str(1 << i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
return str(uint32(x))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceIndex defines alias 'interface_index'.
|
||||||
|
type InterfaceIndex uint32
|
||||||
717
internal/vpp/binapi/ip_types/ip_types.ba.go
Normal file
717
internal/vpp/binapi/ip_types/ip_types.ba.go
Normal file
@@ -0,0 +1,717 @@
|
|||||||
|
// Code generated by GoVPP's binapi-generator. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Package ip_types contains generated bindings for API file ip_types.api.
|
||||||
|
//
|
||||||
|
// Contents:
|
||||||
|
// - 5 aliases
|
||||||
|
// - 5 enums
|
||||||
|
// - 8 structs
|
||||||
|
// - 1 union
|
||||||
|
package ip_types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
api "go.fd.io/govpp/api"
|
||||||
|
codec "go.fd.io/govpp/codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 = "ip_types"
|
||||||
|
APIVersion = "3.0.0"
|
||||||
|
VersionCrc = 0xfee023ed
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddressFamily defines enum 'address_family'.
|
||||||
|
type AddressFamily uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ADDRESS_IP4 AddressFamily = 0
|
||||||
|
ADDRESS_IP6 AddressFamily = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AddressFamily_name = map[uint8]string{
|
||||||
|
0: "ADDRESS_IP4",
|
||||||
|
1: "ADDRESS_IP6",
|
||||||
|
}
|
||||||
|
AddressFamily_value = map[string]uint8{
|
||||||
|
"ADDRESS_IP4": 0,
|
||||||
|
"ADDRESS_IP6": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x AddressFamily) String() string {
|
||||||
|
s, ok := AddressFamily_name[uint8(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "AddressFamily(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPDscp defines enum 'ip_dscp'.
|
||||||
|
type IPDscp uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
IP_API_DSCP_CS0 IPDscp = 0
|
||||||
|
IP_API_DSCP_CS1 IPDscp = 8
|
||||||
|
IP_API_DSCP_AF11 IPDscp = 10
|
||||||
|
IP_API_DSCP_AF12 IPDscp = 12
|
||||||
|
IP_API_DSCP_AF13 IPDscp = 14
|
||||||
|
IP_API_DSCP_CS2 IPDscp = 16
|
||||||
|
IP_API_DSCP_AF21 IPDscp = 18
|
||||||
|
IP_API_DSCP_AF22 IPDscp = 20
|
||||||
|
IP_API_DSCP_AF23 IPDscp = 22
|
||||||
|
IP_API_DSCP_CS3 IPDscp = 24
|
||||||
|
IP_API_DSCP_AF31 IPDscp = 26
|
||||||
|
IP_API_DSCP_AF32 IPDscp = 28
|
||||||
|
IP_API_DSCP_AF33 IPDscp = 30
|
||||||
|
IP_API_DSCP_CS4 IPDscp = 32
|
||||||
|
IP_API_DSCP_AF41 IPDscp = 34
|
||||||
|
IP_API_DSCP_AF42 IPDscp = 36
|
||||||
|
IP_API_DSCP_AF43 IPDscp = 38
|
||||||
|
IP_API_DSCP_CS5 IPDscp = 40
|
||||||
|
IP_API_DSCP_EF IPDscp = 46
|
||||||
|
IP_API_DSCP_CS6 IPDscp = 48
|
||||||
|
IP_API_DSCP_CS7 IPDscp = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IPDscp_name = map[uint8]string{
|
||||||
|
0: "IP_API_DSCP_CS0",
|
||||||
|
8: "IP_API_DSCP_CS1",
|
||||||
|
10: "IP_API_DSCP_AF11",
|
||||||
|
12: "IP_API_DSCP_AF12",
|
||||||
|
14: "IP_API_DSCP_AF13",
|
||||||
|
16: "IP_API_DSCP_CS2",
|
||||||
|
18: "IP_API_DSCP_AF21",
|
||||||
|
20: "IP_API_DSCP_AF22",
|
||||||
|
22: "IP_API_DSCP_AF23",
|
||||||
|
24: "IP_API_DSCP_CS3",
|
||||||
|
26: "IP_API_DSCP_AF31",
|
||||||
|
28: "IP_API_DSCP_AF32",
|
||||||
|
30: "IP_API_DSCP_AF33",
|
||||||
|
32: "IP_API_DSCP_CS4",
|
||||||
|
34: "IP_API_DSCP_AF41",
|
||||||
|
36: "IP_API_DSCP_AF42",
|
||||||
|
38: "IP_API_DSCP_AF43",
|
||||||
|
40: "IP_API_DSCP_CS5",
|
||||||
|
46: "IP_API_DSCP_EF",
|
||||||
|
48: "IP_API_DSCP_CS6",
|
||||||
|
50: "IP_API_DSCP_CS7",
|
||||||
|
}
|
||||||
|
IPDscp_value = map[string]uint8{
|
||||||
|
"IP_API_DSCP_CS0": 0,
|
||||||
|
"IP_API_DSCP_CS1": 8,
|
||||||
|
"IP_API_DSCP_AF11": 10,
|
||||||
|
"IP_API_DSCP_AF12": 12,
|
||||||
|
"IP_API_DSCP_AF13": 14,
|
||||||
|
"IP_API_DSCP_CS2": 16,
|
||||||
|
"IP_API_DSCP_AF21": 18,
|
||||||
|
"IP_API_DSCP_AF22": 20,
|
||||||
|
"IP_API_DSCP_AF23": 22,
|
||||||
|
"IP_API_DSCP_CS3": 24,
|
||||||
|
"IP_API_DSCP_AF31": 26,
|
||||||
|
"IP_API_DSCP_AF32": 28,
|
||||||
|
"IP_API_DSCP_AF33": 30,
|
||||||
|
"IP_API_DSCP_CS4": 32,
|
||||||
|
"IP_API_DSCP_AF41": 34,
|
||||||
|
"IP_API_DSCP_AF42": 36,
|
||||||
|
"IP_API_DSCP_AF43": 38,
|
||||||
|
"IP_API_DSCP_CS5": 40,
|
||||||
|
"IP_API_DSCP_EF": 46,
|
||||||
|
"IP_API_DSCP_CS6": 48,
|
||||||
|
"IP_API_DSCP_CS7": 50,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x IPDscp) String() string {
|
||||||
|
s, ok := IPDscp_name[uint8(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "IPDscp(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPEcn defines enum 'ip_ecn'.
|
||||||
|
type IPEcn uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
IP_API_ECN_NONE IPEcn = 0
|
||||||
|
IP_API_ECN_ECT0 IPEcn = 1
|
||||||
|
IP_API_ECN_ECT1 IPEcn = 2
|
||||||
|
IP_API_ECN_CE IPEcn = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IPEcn_name = map[uint8]string{
|
||||||
|
0: "IP_API_ECN_NONE",
|
||||||
|
1: "IP_API_ECN_ECT0",
|
||||||
|
2: "IP_API_ECN_ECT1",
|
||||||
|
3: "IP_API_ECN_CE",
|
||||||
|
}
|
||||||
|
IPEcn_value = map[string]uint8{
|
||||||
|
"IP_API_ECN_NONE": 0,
|
||||||
|
"IP_API_ECN_ECT0": 1,
|
||||||
|
"IP_API_ECN_ECT1": 2,
|
||||||
|
"IP_API_ECN_CE": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x IPEcn) String() string {
|
||||||
|
s, ok := IPEcn_name[uint8(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "IPEcn(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPFeatureLocation defines enum 'ip_feature_location'.
|
||||||
|
type IPFeatureLocation uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
IP_API_FEATURE_INPUT IPFeatureLocation = 0
|
||||||
|
IP_API_FEATURE_OUTPUT IPFeatureLocation = 1
|
||||||
|
IP_API_FEATURE_LOCAL IPFeatureLocation = 2
|
||||||
|
IP_API_FEATURE_PUNT IPFeatureLocation = 3
|
||||||
|
IP_API_FEATURE_DROP IPFeatureLocation = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IPFeatureLocation_name = map[uint8]string{
|
||||||
|
0: "IP_API_FEATURE_INPUT",
|
||||||
|
1: "IP_API_FEATURE_OUTPUT",
|
||||||
|
2: "IP_API_FEATURE_LOCAL",
|
||||||
|
3: "IP_API_FEATURE_PUNT",
|
||||||
|
4: "IP_API_FEATURE_DROP",
|
||||||
|
}
|
||||||
|
IPFeatureLocation_value = map[string]uint8{
|
||||||
|
"IP_API_FEATURE_INPUT": 0,
|
||||||
|
"IP_API_FEATURE_OUTPUT": 1,
|
||||||
|
"IP_API_FEATURE_LOCAL": 2,
|
||||||
|
"IP_API_FEATURE_PUNT": 3,
|
||||||
|
"IP_API_FEATURE_DROP": 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x IPFeatureLocation) String() string {
|
||||||
|
s, ok := IPFeatureLocation_name[uint8(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "IPFeatureLocation(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPProto defines enum 'ip_proto'.
|
||||||
|
type IPProto uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
IP_API_PROTO_HOPOPT IPProto = 0
|
||||||
|
IP_API_PROTO_ICMP IPProto = 1
|
||||||
|
IP_API_PROTO_IGMP IPProto = 2
|
||||||
|
IP_API_PROTO_TCP IPProto = 6
|
||||||
|
IP_API_PROTO_UDP IPProto = 17
|
||||||
|
IP_API_PROTO_GRE IPProto = 47
|
||||||
|
IP_API_PROTO_ESP IPProto = 50
|
||||||
|
IP_API_PROTO_AH IPProto = 51
|
||||||
|
IP_API_PROTO_ICMP6 IPProto = 58
|
||||||
|
IP_API_PROTO_EIGRP IPProto = 88
|
||||||
|
IP_API_PROTO_OSPF IPProto = 89
|
||||||
|
IP_API_PROTO_SCTP IPProto = 132
|
||||||
|
IP_API_PROTO_RESERVED IPProto = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IPProto_name = map[uint8]string{
|
||||||
|
0: "IP_API_PROTO_HOPOPT",
|
||||||
|
1: "IP_API_PROTO_ICMP",
|
||||||
|
2: "IP_API_PROTO_IGMP",
|
||||||
|
6: "IP_API_PROTO_TCP",
|
||||||
|
17: "IP_API_PROTO_UDP",
|
||||||
|
47: "IP_API_PROTO_GRE",
|
||||||
|
50: "IP_API_PROTO_ESP",
|
||||||
|
51: "IP_API_PROTO_AH",
|
||||||
|
58: "IP_API_PROTO_ICMP6",
|
||||||
|
88: "IP_API_PROTO_EIGRP",
|
||||||
|
89: "IP_API_PROTO_OSPF",
|
||||||
|
132: "IP_API_PROTO_SCTP",
|
||||||
|
255: "IP_API_PROTO_RESERVED",
|
||||||
|
}
|
||||||
|
IPProto_value = map[string]uint8{
|
||||||
|
"IP_API_PROTO_HOPOPT": 0,
|
||||||
|
"IP_API_PROTO_ICMP": 1,
|
||||||
|
"IP_API_PROTO_IGMP": 2,
|
||||||
|
"IP_API_PROTO_TCP": 6,
|
||||||
|
"IP_API_PROTO_UDP": 17,
|
||||||
|
"IP_API_PROTO_GRE": 47,
|
||||||
|
"IP_API_PROTO_ESP": 50,
|
||||||
|
"IP_API_PROTO_AH": 51,
|
||||||
|
"IP_API_PROTO_ICMP6": 58,
|
||||||
|
"IP_API_PROTO_EIGRP": 88,
|
||||||
|
"IP_API_PROTO_OSPF": 89,
|
||||||
|
"IP_API_PROTO_SCTP": 132,
|
||||||
|
"IP_API_PROTO_RESERVED": 255,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x IPProto) String() string {
|
||||||
|
s, ok := IPProto_name[uint8(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "IPProto(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddressWithPrefix defines alias 'address_with_prefix'.
|
||||||
|
type AddressWithPrefix Prefix
|
||||||
|
|
||||||
|
func NewAddressWithPrefix(network net.IPNet) AddressWithPrefix {
|
||||||
|
prefix := NewPrefix(network)
|
||||||
|
return AddressWithPrefix(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAddressWithPrefix(s string) (AddressWithPrefix, error) {
|
||||||
|
prefix, err := ParsePrefix(s)
|
||||||
|
if err != nil {
|
||||||
|
return AddressWithPrefix{}, err
|
||||||
|
}
|
||||||
|
return AddressWithPrefix(prefix), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x AddressWithPrefix) ToIPNet() *net.IPNet {
|
||||||
|
return Prefix(x).ToIPNet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x AddressWithPrefix) String() string {
|
||||||
|
return Prefix(x).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AddressWithPrefix) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AddressWithPrefix) UnmarshalText(text []byte) error {
|
||||||
|
prefix, err := ParseAddressWithPrefix(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = prefix
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP4Address defines alias 'ip4_address'.
|
||||||
|
type IP4Address [4]uint8
|
||||||
|
|
||||||
|
func NewIP4Address(ip net.IP) IP4Address {
|
||||||
|
var ipaddr IP4Address
|
||||||
|
copy(ipaddr[:], ip.To4())
|
||||||
|
return ipaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIP4Address(s string) (IP4Address, error) {
|
||||||
|
ip := net.ParseIP(s).To4()
|
||||||
|
if ip == nil {
|
||||||
|
return IP4Address{}, fmt.Errorf("invalid IP4 address: %s", s)
|
||||||
|
}
|
||||||
|
var ipaddr IP4Address
|
||||||
|
copy(ipaddr[:], ip.To4())
|
||||||
|
return ipaddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP4Address) ToIP() net.IP {
|
||||||
|
return net.IP(x[:]).To4()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP4Address) String() string {
|
||||||
|
return x.ToIP().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP4Address) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP4Address) UnmarshalText(text []byte) error {
|
||||||
|
ipaddr, err := ParseIP4Address(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = ipaddr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP4AddressWithPrefix defines alias 'ip4_address_with_prefix'.
|
||||||
|
type IP4AddressWithPrefix IP4Prefix
|
||||||
|
|
||||||
|
// IP6Address defines alias 'ip6_address'.
|
||||||
|
type IP6Address [16]uint8
|
||||||
|
|
||||||
|
func NewIP6Address(ip net.IP) IP6Address {
|
||||||
|
var ipaddr IP6Address
|
||||||
|
copy(ipaddr[:], ip.To16())
|
||||||
|
return ipaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIP6Address(s string) (IP6Address, error) {
|
||||||
|
ip := net.ParseIP(s).To16()
|
||||||
|
if ip == nil {
|
||||||
|
return IP6Address{}, fmt.Errorf("invalid IP6 address: %s", s)
|
||||||
|
}
|
||||||
|
var ipaddr IP6Address
|
||||||
|
copy(ipaddr[:], ip.To16())
|
||||||
|
return ipaddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP6Address) ToIP() net.IP {
|
||||||
|
return net.IP(x[:]).To16()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP6Address) String() string {
|
||||||
|
return x.ToIP().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP6Address) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP6Address) UnmarshalText(text []byte) error {
|
||||||
|
ipaddr, err := ParseIP6Address(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = ipaddr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP6AddressWithPrefix defines alias 'ip6_address_with_prefix'.
|
||||||
|
type IP6AddressWithPrefix IP6Prefix
|
||||||
|
|
||||||
|
// Address defines type 'address'.
|
||||||
|
type Address struct {
|
||||||
|
Af AddressFamily `binapi:"address_family,name=af" json:"af,omitempty"`
|
||||||
|
Un AddressUnion `binapi:"address_union,name=un" json:"un,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddress(ip net.IP) Address {
|
||||||
|
var addr Address
|
||||||
|
if ip.To4() == nil {
|
||||||
|
addr.Af = ADDRESS_IP6
|
||||||
|
var ip6 IP6Address
|
||||||
|
copy(ip6[:], ip.To16())
|
||||||
|
addr.Un.SetIP6(ip6)
|
||||||
|
} else {
|
||||||
|
addr.Af = ADDRESS_IP4
|
||||||
|
var ip4 IP4Address
|
||||||
|
copy(ip4[:], ip.To4())
|
||||||
|
addr.Un.SetIP4(ip4)
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAddress(s string) (Address, error) {
|
||||||
|
ip := net.ParseIP(s)
|
||||||
|
if ip == nil {
|
||||||
|
return Address{}, fmt.Errorf("invalid IP address: %s", s)
|
||||||
|
}
|
||||||
|
return NewAddress(ip), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Address) ToIP() net.IP {
|
||||||
|
if x.Af == ADDRESS_IP6 {
|
||||||
|
ip6 := x.Un.GetIP6()
|
||||||
|
return net.IP(ip6[:]).To16()
|
||||||
|
} else {
|
||||||
|
ip4 := x.Un.GetIP4()
|
||||||
|
return net.IP(ip4[:]).To4()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Address) String() string {
|
||||||
|
return x.ToIP().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Address) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Address) UnmarshalText(text []byte) error {
|
||||||
|
addr, err := ParseAddress(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = addr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP4AddressAndMask defines type 'ip4_address_and_mask'.
|
||||||
|
type IP4AddressAndMask struct {
|
||||||
|
Addr IP4Address `binapi:"ip4_address,name=addr" json:"addr,omitempty"`
|
||||||
|
Mask IP4Address `binapi:"ip4_address,name=mask" json:"mask,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP4Prefix defines type 'ip4_prefix'.
|
||||||
|
type IP4Prefix struct {
|
||||||
|
Address IP4Address `binapi:"ip4_address,name=address" json:"address,omitempty"`
|
||||||
|
Len uint8 `binapi:"u8,name=len" json:"len,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIP4Prefix(network net.IPNet) IP4Prefix {
|
||||||
|
var prefix IP4Prefix
|
||||||
|
maskSize, _ := network.Mask.Size()
|
||||||
|
prefix.Len = byte(maskSize)
|
||||||
|
prefix.Address = NewIP4Address(network.IP)
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIP4Prefix(s string) (prefix IP4Prefix, err error) {
|
||||||
|
hasPrefix := strings.Contains(s, "/")
|
||||||
|
if hasPrefix {
|
||||||
|
ip, network, err := net.ParseCIDR(s)
|
||||||
|
if err != nil {
|
||||||
|
return IP4Prefix{}, fmt.Errorf("invalid IP4 %s: %s", s, err)
|
||||||
|
}
|
||||||
|
maskSize, _ := network.Mask.Size()
|
||||||
|
prefix.Len = byte(maskSize)
|
||||||
|
prefix.Address, err = ParseIP4Address(ip.String())
|
||||||
|
if err != nil {
|
||||||
|
return IP4Prefix{}, fmt.Errorf("invalid IP4 %s: %s", s, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ip := net.ParseIP(s)
|
||||||
|
defaultMaskSize, _ := net.CIDRMask(32, 32).Size()
|
||||||
|
if ip.To4() == nil {
|
||||||
|
defaultMaskSize, _ = net.CIDRMask(128, 128).Size()
|
||||||
|
}
|
||||||
|
prefix.Len = byte(defaultMaskSize)
|
||||||
|
prefix.Address, err = ParseIP4Address(ip.String())
|
||||||
|
if err != nil {
|
||||||
|
return IP4Prefix{}, fmt.Errorf("invalid IP4 %s: %s", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP4Prefix) ToIPNet() *net.IPNet {
|
||||||
|
mask := net.CIDRMask(int(x.Len), 32)
|
||||||
|
ipnet := &net.IPNet{IP: x.Address.ToIP(), Mask: mask}
|
||||||
|
return ipnet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP4Prefix) String() string {
|
||||||
|
ip := x.Address.String()
|
||||||
|
return ip + "/" + strconv.Itoa(int(x.Len))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP4Prefix) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP4Prefix) UnmarshalText(text []byte) error {
|
||||||
|
prefix, err := ParseIP4Prefix(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = prefix
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP6AddressAndMask defines type 'ip6_address_and_mask'.
|
||||||
|
type IP6AddressAndMask struct {
|
||||||
|
Addr IP6Address `binapi:"ip6_address,name=addr" json:"addr,omitempty"`
|
||||||
|
Mask IP6Address `binapi:"ip6_address,name=mask" json:"mask,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP6Prefix defines type 'ip6_prefix'.
|
||||||
|
type IP6Prefix struct {
|
||||||
|
Address IP6Address `binapi:"ip6_address,name=address" json:"address,omitempty"`
|
||||||
|
Len uint8 `binapi:"u8,name=len" json:"len,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIP6Prefix(network net.IPNet) IP6Prefix {
|
||||||
|
var prefix IP6Prefix
|
||||||
|
maskSize, _ := network.Mask.Size()
|
||||||
|
prefix.Len = byte(maskSize)
|
||||||
|
prefix.Address = NewIP6Address(network.IP)
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIP6Prefix(s string) (prefix IP6Prefix, err error) {
|
||||||
|
hasPrefix := strings.Contains(s, "/")
|
||||||
|
if hasPrefix {
|
||||||
|
ip, network, err := net.ParseCIDR(s)
|
||||||
|
if err != nil {
|
||||||
|
return IP6Prefix{}, fmt.Errorf("invalid IP6 %s: %s", s, err)
|
||||||
|
}
|
||||||
|
maskSize, _ := network.Mask.Size()
|
||||||
|
prefix.Len = byte(maskSize)
|
||||||
|
prefix.Address, err = ParseIP6Address(ip.String())
|
||||||
|
if err != nil {
|
||||||
|
return IP6Prefix{}, fmt.Errorf("invalid IP6 %s: %s", s, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ip := net.ParseIP(s)
|
||||||
|
defaultMaskSize, _ := net.CIDRMask(32, 32).Size()
|
||||||
|
if ip.To4() == nil {
|
||||||
|
defaultMaskSize, _ = net.CIDRMask(128, 128).Size()
|
||||||
|
}
|
||||||
|
prefix.Len = byte(defaultMaskSize)
|
||||||
|
prefix.Address, err = ParseIP6Address(ip.String())
|
||||||
|
if err != nil {
|
||||||
|
return IP6Prefix{}, fmt.Errorf("invalid IP6 %s: %s", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP6Prefix) ToIPNet() *net.IPNet {
|
||||||
|
mask := net.CIDRMask(int(x.Len), 128)
|
||||||
|
ipnet := &net.IPNet{IP: x.Address.ToIP(), Mask: mask}
|
||||||
|
return ipnet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x IP6Prefix) String() string {
|
||||||
|
ip := x.Address.String()
|
||||||
|
return ip + "/" + strconv.Itoa(int(x.Len))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP6Prefix) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *IP6Prefix) UnmarshalText(text []byte) error {
|
||||||
|
prefix, err := ParseIP6Prefix(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = prefix
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mprefix defines type 'mprefix'.
|
||||||
|
type Mprefix struct {
|
||||||
|
Af AddressFamily `binapi:"address_family,name=af" json:"af,omitempty"`
|
||||||
|
GrpAddressLength uint16 `binapi:"u16,name=grp_address_length" json:"grp_address_length,omitempty"`
|
||||||
|
GrpAddress AddressUnion `binapi:"address_union,name=grp_address" json:"grp_address,omitempty"`
|
||||||
|
SrcAddress AddressUnion `binapi:"address_union,name=src_address" json:"src_address,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix defines type 'prefix'.
|
||||||
|
type Prefix struct {
|
||||||
|
Address Address `binapi:"address,name=address" json:"address,omitempty"`
|
||||||
|
Len uint8 `binapi:"u8,name=len" json:"len,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPrefix(network net.IPNet) Prefix {
|
||||||
|
var prefix Prefix
|
||||||
|
maskSize, _ := network.Mask.Size()
|
||||||
|
prefix.Len = byte(maskSize)
|
||||||
|
prefix.Address = NewAddress(network.IP)
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsePrefix(ip string) (prefix Prefix, err error) {
|
||||||
|
hasPrefix := strings.Contains(ip, "/")
|
||||||
|
if hasPrefix {
|
||||||
|
netIP, network, err := net.ParseCIDR(ip)
|
||||||
|
if err != nil {
|
||||||
|
return Prefix{}, fmt.Errorf("invalid IP %s: %s", ip, err)
|
||||||
|
}
|
||||||
|
maskSize, _ := network.Mask.Size()
|
||||||
|
prefix.Len = byte(maskSize)
|
||||||
|
prefix.Address, err = ParseAddress(netIP.String())
|
||||||
|
if err != nil {
|
||||||
|
return Prefix{}, fmt.Errorf("invalid IP %s: %s", ip, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
netIP := net.ParseIP(ip)
|
||||||
|
defaultMaskSize, _ := net.CIDRMask(32, 32).Size()
|
||||||
|
if netIP.To4() == nil {
|
||||||
|
defaultMaskSize, _ = net.CIDRMask(128, 128).Size()
|
||||||
|
}
|
||||||
|
prefix.Len = byte(defaultMaskSize)
|
||||||
|
prefix.Address, err = ParseAddress(netIP.String())
|
||||||
|
if err != nil {
|
||||||
|
return Prefix{}, fmt.Errorf("invalid IP %s: %s", ip, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Prefix) ToIPNet() *net.IPNet {
|
||||||
|
var mask net.IPMask
|
||||||
|
if x.Address.Af == ADDRESS_IP4 {
|
||||||
|
mask = net.CIDRMask(int(x.Len), 32)
|
||||||
|
} else {
|
||||||
|
mask = net.CIDRMask(int(x.Len), 128)
|
||||||
|
}
|
||||||
|
ipnet := &net.IPNet{IP: x.Address.ToIP(), Mask: mask}
|
||||||
|
return ipnet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Prefix) String() string {
|
||||||
|
ip := x.Address.String()
|
||||||
|
return ip + "/" + strconv.Itoa(int(x.Len))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Prefix) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Prefix) UnmarshalText(text []byte) error {
|
||||||
|
prefix, err := ParsePrefix(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = prefix
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefixMatcher defines type 'prefix_matcher'.
|
||||||
|
type PrefixMatcher struct {
|
||||||
|
Le uint8 `binapi:"u8,name=le" json:"le,omitempty"`
|
||||||
|
Ge uint8 `binapi:"u8,name=ge" json:"ge,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddressUnion defines union 'address_union'.
|
||||||
|
type AddressUnion struct {
|
||||||
|
// AddressUnion can be one of:
|
||||||
|
// - IP4 *IP4Address
|
||||||
|
// - IP6 *IP6Address
|
||||||
|
XXX_UnionData [16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddressUnionIP4(a IP4Address) (u AddressUnion) {
|
||||||
|
u.SetIP4(a)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (u *AddressUnion) SetIP4(a IP4Address) {
|
||||||
|
buf := codec.NewBuffer(u.XXX_UnionData[:])
|
||||||
|
buf.EncodeBytes(a[:], 4)
|
||||||
|
}
|
||||||
|
func (u *AddressUnion) GetIP4() (a IP4Address) {
|
||||||
|
buf := codec.NewBuffer(u.XXX_UnionData[:])
|
||||||
|
copy(a[:], buf.DecodeBytes(4))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddressUnionIP6(a IP6Address) (u AddressUnion) {
|
||||||
|
u.SetIP6(a)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (u *AddressUnion) SetIP6(a IP6Address) {
|
||||||
|
buf := codec.NewBuffer(u.XXX_UnionData[:])
|
||||||
|
buf.EncodeBytes(a[:], 16)
|
||||||
|
}
|
||||||
|
func (u *AddressUnion) GetIP6() (a IP6Address) {
|
||||||
|
buf := codec.NewBuffer(u.XXX_UnionData[:])
|
||||||
|
copy(a[:], buf.DecodeBytes(16))
|
||||||
|
return
|
||||||
|
}
|
||||||
1415
internal/vpp/binapi/lb/lb.ba.go
Normal file
1415
internal/vpp/binapi/lb/lb.ba.go
Normal file
File diff suppressed because it is too large
Load Diff
211
internal/vpp/binapi/lb_types/lb_types.ba.go
Normal file
211
internal/vpp/binapi/lb_types/lb_types.ba.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
// Code generated by GoVPP's binapi-generator. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Package lb_types contains generated bindings for API file lb_types.api.
|
||||||
|
//
|
||||||
|
// Contents:
|
||||||
|
// - 5 enums
|
||||||
|
// - 1 struct
|
||||||
|
package lb_types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
ip_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/ip_types"
|
||||||
|
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 = "lb_types"
|
||||||
|
APIVersion = "1.0.0"
|
||||||
|
VersionCrc = 0xba19340c
|
||||||
|
)
|
||||||
|
|
||||||
|
// LbEncapType defines enum 'lb_encap_type'.
|
||||||
|
type LbEncapType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LB_API_ENCAP_TYPE_GRE4 LbEncapType = 0
|
||||||
|
LB_API_ENCAP_TYPE_GRE6 LbEncapType = 1
|
||||||
|
LB_API_ENCAP_TYPE_L3DSR LbEncapType = 2
|
||||||
|
LB_API_ENCAP_TYPE_NAT4 LbEncapType = 3
|
||||||
|
LB_API_ENCAP_TYPE_NAT6 LbEncapType = 4
|
||||||
|
LB_API_ENCAP_N_TYPES LbEncapType = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LbEncapType_name = map[uint32]string{
|
||||||
|
0: "LB_API_ENCAP_TYPE_GRE4",
|
||||||
|
1: "LB_API_ENCAP_TYPE_GRE6",
|
||||||
|
2: "LB_API_ENCAP_TYPE_L3DSR",
|
||||||
|
3: "LB_API_ENCAP_TYPE_NAT4",
|
||||||
|
4: "LB_API_ENCAP_TYPE_NAT6",
|
||||||
|
5: "LB_API_ENCAP_N_TYPES",
|
||||||
|
}
|
||||||
|
LbEncapType_value = map[string]uint32{
|
||||||
|
"LB_API_ENCAP_TYPE_GRE4": 0,
|
||||||
|
"LB_API_ENCAP_TYPE_GRE6": 1,
|
||||||
|
"LB_API_ENCAP_TYPE_L3DSR": 2,
|
||||||
|
"LB_API_ENCAP_TYPE_NAT4": 3,
|
||||||
|
"LB_API_ENCAP_TYPE_NAT6": 4,
|
||||||
|
"LB_API_ENCAP_N_TYPES": 5,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x LbEncapType) String() string {
|
||||||
|
s, ok := LbEncapType_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "LbEncapType(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LbLkpTypeT defines enum 'lb_lkp_type_t'.
|
||||||
|
type LbLkpTypeT uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LB_API_LKP_SAME_IP_PORT LbLkpTypeT = 0
|
||||||
|
LB_API_LKP_DIFF_IP_PORT LbLkpTypeT = 1
|
||||||
|
LB_API_LKP_ALL_PORT_IP LbLkpTypeT = 2
|
||||||
|
LB_API_LKP_N_TYPES LbLkpTypeT = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LbLkpTypeT_name = map[uint32]string{
|
||||||
|
0: "LB_API_LKP_SAME_IP_PORT",
|
||||||
|
1: "LB_API_LKP_DIFF_IP_PORT",
|
||||||
|
2: "LB_API_LKP_ALL_PORT_IP",
|
||||||
|
3: "LB_API_LKP_N_TYPES",
|
||||||
|
}
|
||||||
|
LbLkpTypeT_value = map[string]uint32{
|
||||||
|
"LB_API_LKP_SAME_IP_PORT": 0,
|
||||||
|
"LB_API_LKP_DIFF_IP_PORT": 1,
|
||||||
|
"LB_API_LKP_ALL_PORT_IP": 2,
|
||||||
|
"LB_API_LKP_N_TYPES": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x LbLkpTypeT) String() string {
|
||||||
|
s, ok := LbLkpTypeT_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "LbLkpTypeT(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LbNatProtocol defines enum 'lb_nat_protocol'.
|
||||||
|
type LbNatProtocol uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LB_API_NAT_PROTOCOL_UDP LbNatProtocol = 6
|
||||||
|
LB_API_NAT_PROTOCOL_TCP LbNatProtocol = 23
|
||||||
|
LB_API_NAT_PROTOCOL_ANY LbNatProtocol = 4294967295
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LbNatProtocol_name = map[uint32]string{
|
||||||
|
6: "LB_API_NAT_PROTOCOL_UDP",
|
||||||
|
23: "LB_API_NAT_PROTOCOL_TCP",
|
||||||
|
4294967295: "LB_API_NAT_PROTOCOL_ANY",
|
||||||
|
}
|
||||||
|
LbNatProtocol_value = map[string]uint32{
|
||||||
|
"LB_API_NAT_PROTOCOL_UDP": 6,
|
||||||
|
"LB_API_NAT_PROTOCOL_TCP": 23,
|
||||||
|
"LB_API_NAT_PROTOCOL_ANY": 4294967295,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x LbNatProtocol) String() string {
|
||||||
|
s, ok := LbNatProtocol_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "LbNatProtocol(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LbSrvType defines enum 'lb_srv_type'.
|
||||||
|
type LbSrvType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LB_API_SRV_TYPE_CLUSTERIP LbSrvType = 0
|
||||||
|
LB_API_SRV_TYPE_NODEPORT LbSrvType = 1
|
||||||
|
LB_API_SRV_N_TYPES LbSrvType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LbSrvType_name = map[uint32]string{
|
||||||
|
0: "LB_API_SRV_TYPE_CLUSTERIP",
|
||||||
|
1: "LB_API_SRV_TYPE_NODEPORT",
|
||||||
|
2: "LB_API_SRV_N_TYPES",
|
||||||
|
}
|
||||||
|
LbSrvType_value = map[string]uint32{
|
||||||
|
"LB_API_SRV_TYPE_CLUSTERIP": 0,
|
||||||
|
"LB_API_SRV_TYPE_NODEPORT": 1,
|
||||||
|
"LB_API_SRV_N_TYPES": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x LbSrvType) String() string {
|
||||||
|
s, ok := LbSrvType_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "LbSrvType(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LbVipType defines enum 'lb_vip_type'.
|
||||||
|
type LbVipType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LB_API_VIP_TYPE_IP6_GRE6 LbVipType = 0
|
||||||
|
LB_API_VIP_TYPE_IP6_GRE4 LbVipType = 1
|
||||||
|
LB_API_VIP_TYPE_IP4_GRE6 LbVipType = 2
|
||||||
|
LB_API_VIP_TYPE_IP4_GRE4 LbVipType = 3
|
||||||
|
LB_API_VIP_TYPE_IP4_L3DSR LbVipType = 4
|
||||||
|
LB_API_VIP_TYPE_IP4_NAT4 LbVipType = 5
|
||||||
|
LB_API_VIP_TYPE_IP6_NAT6 LbVipType = 6
|
||||||
|
LB_API_VIP_N_TYPES LbVipType = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LbVipType_name = map[uint32]string{
|
||||||
|
0: "LB_API_VIP_TYPE_IP6_GRE6",
|
||||||
|
1: "LB_API_VIP_TYPE_IP6_GRE4",
|
||||||
|
2: "LB_API_VIP_TYPE_IP4_GRE6",
|
||||||
|
3: "LB_API_VIP_TYPE_IP4_GRE4",
|
||||||
|
4: "LB_API_VIP_TYPE_IP4_L3DSR",
|
||||||
|
5: "LB_API_VIP_TYPE_IP4_NAT4",
|
||||||
|
6: "LB_API_VIP_TYPE_IP6_NAT6",
|
||||||
|
7: "LB_API_VIP_N_TYPES",
|
||||||
|
}
|
||||||
|
LbVipType_value = map[string]uint32{
|
||||||
|
"LB_API_VIP_TYPE_IP6_GRE6": 0,
|
||||||
|
"LB_API_VIP_TYPE_IP6_GRE4": 1,
|
||||||
|
"LB_API_VIP_TYPE_IP4_GRE6": 2,
|
||||||
|
"LB_API_VIP_TYPE_IP4_GRE4": 3,
|
||||||
|
"LB_API_VIP_TYPE_IP4_L3DSR": 4,
|
||||||
|
"LB_API_VIP_TYPE_IP4_NAT4": 5,
|
||||||
|
"LB_API_VIP_TYPE_IP6_NAT6": 6,
|
||||||
|
"LB_API_VIP_N_TYPES": 7,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x LbVipType) String() string {
|
||||||
|
s, ok := LbVipType_name[uint32(x)]
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "LbVipType(" + strconv.Itoa(int(x)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LbVip defines type 'lb_vip'.
|
||||||
|
type LbVip struct {
|
||||||
|
Pfx ip_types.AddressWithPrefix `binapi:"address_with_prefix,name=pfx" json:"pfx,omitempty"`
|
||||||
|
Protocol ip_types.IPProto `binapi:"ip_proto,name=protocol" json:"protocol,omitempty"`
|
||||||
|
Port uint16 `binapi:"u16,name=port" json:"port,omitempty"`
|
||||||
|
}
|
||||||
@@ -14,13 +14,23 @@ import (
|
|||||||
"go.fd.io/govpp/adapter"
|
"go.fd.io/govpp/adapter"
|
||||||
"go.fd.io/govpp/adapter/socketclient"
|
"go.fd.io/govpp/adapter/socketclient"
|
||||||
"go.fd.io/govpp/adapter/statsclient"
|
"go.fd.io/govpp/adapter/statsclient"
|
||||||
"go.fd.io/govpp/api"
|
|
||||||
"go.fd.io/govpp/binapi/vpe"
|
"go.fd.io/govpp/binapi/vpe"
|
||||||
"go.fd.io/govpp/core"
|
"go.fd.io/govpp/core"
|
||||||
|
|
||||||
|
"git.ipng.ch/ipng/vpp-maglev/internal/config"
|
||||||
|
lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ConfigSource provides a snapshot of the current maglev config to the VPP
|
||||||
|
// sync loop. checker.Checker satisfies this interface via its Config() method.
|
||||||
|
// Decoupling via an interface avoids an import cycle with the checker package.
|
||||||
|
type ConfigSource interface {
|
||||||
|
Config() *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
const retryInterval = 5 * time.Second
|
const retryInterval = 5 * time.Second
|
||||||
const pingInterval = 10 * time.Second
|
const pingInterval = 10 * time.Second
|
||||||
|
const defaultLBSyncInterval = 30 * time.Second
|
||||||
|
|
||||||
// Info holds VPP version and connection metadata, populated on connect.
|
// Info holds VPP version and connection metadata, populated on connect.
|
||||||
type Info struct {
|
type Info struct {
|
||||||
@@ -44,6 +54,17 @@ type Client struct {
|
|||||||
statsConn *core.StatsConnection
|
statsConn *core.StatsConnection
|
||||||
statsClient adapter.StatsAPI // raw adapter for DumpStats
|
statsClient adapter.StatsAPI // raw adapter for DumpStats
|
||||||
info Info // populated on successful connect
|
info Info // populated on successful connect
|
||||||
|
cfgSrc ConfigSource // optional; enables periodic LB sync
|
||||||
|
lastLBConf *lb.LbConf // cached last-pushed lb_conf (dedup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigSource attaches a live config source. When set, the VPP client
|
||||||
|
// runs a periodic SyncLBStateAll loop (at the interval from cfg.VPP.LB.SyncInterval)
|
||||||
|
// for as long as the VPP connection is up. Must be called before Run.
|
||||||
|
func (c *Client) SetConfigSource(src ConfigSource) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.cfgSrc = src
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a Client for the given socket paths.
|
// New creates a Client for the given socket paths.
|
||||||
@@ -77,8 +98,43 @@ func (c *Client) Run(ctx context.Context) {
|
|||||||
"pid", c.info.PID,
|
"pid", c.info.PID,
|
||||||
"api", c.apiAddr, "stats", c.statsAddr)
|
"api", c.apiAddr, "stats", c.statsAddr)
|
||||||
|
|
||||||
|
// Read the current LB plugin state so we can log what's programmed.
|
||||||
|
if state, err := c.GetLBStateAll(); err != nil {
|
||||||
|
slog.Warn("vpp-lb-read-failed", "err", err)
|
||||||
|
} else {
|
||||||
|
totalAS := 0
|
||||||
|
for _, v := range state.VIPs {
|
||||||
|
totalAS += len(v.ASes)
|
||||||
|
}
|
||||||
|
slog.Info("vpp-lb-state",
|
||||||
|
"vips", len(state.VIPs),
|
||||||
|
"application-servers", totalAS,
|
||||||
|
"sticky-buckets-per-core", state.Conf.StickyBucketsPerCore,
|
||||||
|
"flow-timeout", state.Conf.FlowTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push global LB conf (src addresses, buckets, timeout) from the
|
||||||
|
// running config. On startup this is the initial set; on reconnect
|
||||||
|
// (VPP restart) VPP has forgotten everything, so we set it again.
|
||||||
|
c.mu.Lock()
|
||||||
|
src := c.cfgSrc
|
||||||
|
c.mu.Unlock()
|
||||||
|
if src != nil {
|
||||||
|
if cfg := src.Config(); cfg != nil {
|
||||||
|
if err := c.SetLBConf(cfg); err != nil {
|
||||||
|
slog.Warn("vpp-lb-conf-set-failed", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the LB sync loop for as long as the connection is up.
|
||||||
|
// It exits when connCtx is cancelled (on disconnect or shutdown).
|
||||||
|
connCtx, connCancel := context.WithCancel(ctx)
|
||||||
|
go c.lbSyncLoop(connCtx)
|
||||||
|
|
||||||
// Hold the connection, pinging periodically to detect VPP restarts.
|
// Hold the connection, pinging periodically to detect VPP restarts.
|
||||||
c.monitor(ctx)
|
c.monitor(ctx)
|
||||||
|
connCancel()
|
||||||
|
|
||||||
// If ctx is done we're shutting down; otherwise VPP dropped and we retry.
|
// If ctx is done we're shutting down; otherwise VPP dropped and we retry.
|
||||||
c.disconnect()
|
c.disconnect()
|
||||||
@@ -89,6 +145,49 @@ func (c *Client) Run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lbSyncLoop periodically runs SyncLBStateAll to catch drift between the
|
||||||
|
// maglev config and the VPP dataplane. The first run happens immediately
|
||||||
|
// on loop start (VPP has just connected, so any pre-existing state needs
|
||||||
|
// reconciliation). Subsequent runs fire every cfg.VPP.LB.SyncInterval.
|
||||||
|
// Exits when ctx is cancelled.
|
||||||
|
func (c *Client) lbSyncLoop(ctx context.Context) {
|
||||||
|
c.mu.Lock()
|
||||||
|
src := c.cfgSrc
|
||||||
|
c.mu.Unlock()
|
||||||
|
if src == nil {
|
||||||
|
return // no config source registered; nothing to sync
|
||||||
|
}
|
||||||
|
|
||||||
|
// next-run timestamp starts at "now" so the first tick is immediate.
|
||||||
|
next := time.Now()
|
||||||
|
for {
|
||||||
|
wait := time.Until(next)
|
||||||
|
if wait < 0 {
|
||||||
|
wait = 0
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(wait):
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := src.Config()
|
||||||
|
if cfg == nil {
|
||||||
|
next = time.Now().Add(defaultLBSyncInterval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
interval := cfg.VPP.LB.SyncInterval
|
||||||
|
if interval <= 0 {
|
||||||
|
interval = defaultLBSyncInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.SyncLBStateAll(cfg); err != nil {
|
||||||
|
slog.Warn("vpp-lbsync-error", "err", err)
|
||||||
|
}
|
||||||
|
next = time.Now().Add(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IsConnected returns true if both API and stats connections are active.
|
// IsConnected returns true if both API and stats connections are active.
|
||||||
func (c *Client) IsConnected() bool {
|
func (c *Client) IsConnected() bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -96,25 +195,6 @@ func (c *Client) IsConnected() bool {
|
|||||||
return c.apiConn != nil && c.statsConn != nil
|
return c.apiConn != nil && c.statsConn != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPIChannel creates a new API channel for sending VPP binary API requests.
|
|
||||||
// Returns an error if the API connection is not established.
|
|
||||||
func (c *Client) NewAPIChannel() (api.Channel, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
conn := c.apiConn
|
|
||||||
c.mu.Unlock()
|
|
||||||
if conn == nil {
|
|
||||||
return nil, errNotConnected
|
|
||||||
}
|
|
||||||
return conn.NewAPIChannel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatsConnection returns the stats connection, or nil if not connected.
|
|
||||||
func (c *Client) StatsConnection() *core.StatsConnection {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.statsConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInfo returns the VPP version and connection metadata, or an error
|
// GetInfo returns the VPP version and connection metadata, or an error
|
||||||
// if VPP is not connected.
|
// if VPP is not connected.
|
||||||
func (c *Client) GetInfo() (Info, error) {
|
func (c *Client) GetInfo() (Info, error) {
|
||||||
@@ -160,6 +240,7 @@ func (c *Client) disconnect() {
|
|||||||
c.statsConn = nil
|
c.statsConn = nil
|
||||||
c.statsClient = nil
|
c.statsClient = nil
|
||||||
c.info = Info{}
|
c.info = Info{}
|
||||||
|
c.lastLBConf = nil // force re-push of lb_conf on reconnect
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
safeDisconnectAPI(apiConn)
|
safeDisconnectAPI(apiConn)
|
||||||
@@ -184,7 +265,7 @@ func (c *Client) monitor(ctx context.Context) {
|
|||||||
|
|
||||||
// ping sends a control_ping to VPP and returns true if it succeeds.
|
// ping sends a control_ping to VPP and returns true if it succeeds.
|
||||||
func (c *Client) ping() bool {
|
func (c *Client) ping() bool {
|
||||||
ch, err := c.NewAPIChannel()
|
ch, err := c.apiChannel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -204,7 +285,7 @@ func (c *Client) ping() bool {
|
|||||||
func (c *Client) fetchInfo() Info {
|
func (c *Client) fetchInfo() Info {
|
||||||
info := Info{ConnectedSince: time.Now()}
|
info := Info{ConnectedSince: time.Now()}
|
||||||
|
|
||||||
ch, err := c.NewAPIChannel()
|
ch, err := c.apiChannel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|||||||
103
internal/vpp/lbconf.go
Normal file
103
internal/vpp/lbconf.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
267
internal/vpp/lbstate.go
Normal file
267
internal/vpp/lbstate.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
477
internal/vpp/lbsync.go
Normal file
477
internal/vpp/lbsync.go
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ service Maglev {
|
|||||||
rpc CheckConfig(CheckConfigRequest) returns (CheckConfigResponse);
|
rpc CheckConfig(CheckConfigRequest) returns (CheckConfigResponse);
|
||||||
rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse);
|
rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse);
|
||||||
rpc GetVPPInfo(GetVPPInfoRequest) returns (VPPInfo);
|
rpc GetVPPInfo(GetVPPInfoRequest) returns (VPPInfo);
|
||||||
|
rpc GetVPPLBState(GetVPPLBStateRequest) returns (VPPLBState);
|
||||||
|
rpc SyncVPPLBState(SyncVPPLBStateRequest) returns (SyncVPPLBStateResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- requests ---------------------------------------------------------------
|
// ---- requests ---------------------------------------------------------------
|
||||||
@@ -75,6 +77,55 @@ message VPPInfo {
|
|||||||
int64 connecttime_ns = 6; // unix timestamp (ns) when maglevd connected to VPP
|
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 {
|
message SetWeightRequest {
|
||||||
string frontend = 1;
|
string frontend = 1;
|
||||||
string pool = 2;
|
string pool = 2;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ maglev:
|
|||||||
healthchecker:
|
healthchecker:
|
||||||
transition-history: 5
|
transition-history: 5
|
||||||
|
|
||||||
|
vpp:
|
||||||
|
lb:
|
||||||
|
ipv4-src-address: 10.0.0.1
|
||||||
|
ipv6-src-address: 2001:db8::1
|
||||||
|
|
||||||
healthchecks:
|
healthchecks:
|
||||||
http-check:
|
http-check:
|
||||||
type: http
|
type: http
|
||||||
|
|||||||
Reference in New Issue
Block a user