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:
@@ -152,6 +152,15 @@ func (c *Checker) Subscribe() (<-chan Event, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
// Config returns the live config pointer held by the checker. Callers must
|
||||
// treat the returned value as read-only. The pointer is swapped on Reload,
|
||||
// so callers that cache it across reloads may see stale data.
|
||||
func (c *Checker) Config() *config.Config {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.cfg
|
||||
}
|
||||
|
||||
// ListFrontends returns the names of all configured frontends.
|
||||
func (c *Checker) ListFrontends() []string {
|
||||
c.mu.RLock()
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
// Config is the top-level parsed and validated configuration.
|
||||
type Config struct {
|
||||
HealthChecker HealthCheckerConfig
|
||||
VPP VPPConfig
|
||||
HealthChecks map[string]HealthCheck
|
||||
Backends map[string]Backend
|
||||
Frontends map[string]Frontend
|
||||
@@ -28,6 +29,37 @@ type HealthCheckerConfig struct {
|
||||
Netns string // network namespace for probes; "" = current netns
|
||||
}
|
||||
|
||||
// VPPConfig holds VPP-related configuration.
|
||||
type VPPConfig struct {
|
||||
LB VPPLBConfig
|
||||
}
|
||||
|
||||
// VPPLBConfig holds load-balancer integration settings.
|
||||
type VPPLBConfig struct {
|
||||
// SyncInterval is how often the full dataplane reconciliation runs,
|
||||
// catching drift (e.g. manual changes to VPP). Defaults to 30s.
|
||||
SyncInterval time.Duration
|
||||
|
||||
// IPv4SrcAddress is the source address VPP uses when encapsulating
|
||||
// IPv4 traffic into GRE4 tunnels to application servers. Required
|
||||
// when any frontend uses an IPv4 VIP; VPP GRE encap will fail if unset.
|
||||
IPv4SrcAddress net.IP
|
||||
|
||||
// IPv6SrcAddress is the source address VPP uses when encapsulating
|
||||
// IPv6 traffic into GRE6 tunnels. Required when any frontend uses an
|
||||
// IPv6 VIP; VPP GRE encap will fail if unset.
|
||||
IPv6SrcAddress net.IP
|
||||
|
||||
// StickyBucketsPerCore is the number of buckets (per worker thread) in
|
||||
// the established-flow table. Must be a power of 2. Defaults to 65536.
|
||||
StickyBucketsPerCore uint32
|
||||
|
||||
// FlowTimeout is the idle time after which an established flow is
|
||||
// removed from the table. Must be between 1 and 120 seconds inclusive.
|
||||
// Defaults to 40s.
|
||||
FlowTimeout time.Duration
|
||||
}
|
||||
|
||||
// HealthCheck describes how to probe a backend.
|
||||
type HealthCheck struct {
|
||||
Type string
|
||||
@@ -97,6 +129,7 @@ type rawConfig struct {
|
||||
|
||||
type rawMaglev struct {
|
||||
HealthChecker rawHealthCheckerCfg `yaml:"healthchecker"`
|
||||
VPP rawVPPCfg `yaml:"vpp"`
|
||||
HealthChecks map[string]rawHealthCheck `yaml:"healthchecks"`
|
||||
Backends map[string]rawBackend `yaml:"backends"`
|
||||
Frontends map[string]rawFrontend `yaml:"frontends"`
|
||||
@@ -107,6 +140,18 @@ type rawHealthCheckerCfg struct {
|
||||
Netns string `yaml:"netns"`
|
||||
}
|
||||
|
||||
type rawVPPCfg struct {
|
||||
LB rawVPPLBCfg `yaml:"lb"`
|
||||
}
|
||||
|
||||
type rawVPPLBCfg struct {
|
||||
SyncInterval string `yaml:"sync-interval"` // Go duration; default 30s
|
||||
IPv4SrcAddress string `yaml:"ipv4-src-address"`
|
||||
IPv6SrcAddress string `yaml:"ipv6-src-address"`
|
||||
StickyBucketsPerCore *uint32 `yaml:"sticky-buckets-per-core"` // default 65536
|
||||
FlowTimeout string `yaml:"flow-timeout"` // Go duration; default 40s, [1-120]s
|
||||
}
|
||||
|
||||
type rawHealthCheck struct {
|
||||
Type string `yaml:"type"`
|
||||
Port uint16 `yaml:"port"`
|
||||
@@ -255,9 +300,93 @@ func convert(r *rawMaglev) (*Config, error) {
|
||||
cfg.Frontends[name] = fe
|
||||
}
|
||||
|
||||
// ---- vpp ------------------------------------------------------------------
|
||||
// Runs last so structural errors in healthchecks/backends/frontends are
|
||||
// reported first; operators fix those, then we tell them about the VPP
|
||||
// src-address requirements.
|
||||
if err := convertVPP(&r.VPP, &cfg.VPP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// convertVPP parses and validates the maglev.vpp section. Missing src-address
|
||||
// fields are tolerated but logged at ERROR level so operators notice that VPP
|
||||
// GRE encap will fail without them.
|
||||
func convertVPP(r *rawVPPCfg, cfg *VPPConfig) error {
|
||||
// sync-interval: default 30s, must be > 0.
|
||||
if s := r.LB.SyncInterval; s != "" {
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("vpp.lb.sync-interval: %w", err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return fmt.Errorf("vpp.lb.sync-interval must be > 0")
|
||||
}
|
||||
cfg.LB.SyncInterval = d
|
||||
} else {
|
||||
cfg.LB.SyncInterval = 30 * time.Second
|
||||
}
|
||||
|
||||
// ipv4-src-address: optional here, but warned below if missing.
|
||||
if s := r.LB.IPv4SrcAddress; s != "" {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return fmt.Errorf("vpp.lb.ipv4-src-address: %q is not a valid IPv4 address", s)
|
||||
}
|
||||
cfg.LB.IPv4SrcAddress = ip.To4()
|
||||
}
|
||||
|
||||
// ipv6-src-address: optional here, but warned below if missing.
|
||||
if s := r.LB.IPv6SrcAddress; s != "" {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil || ip.To4() != nil {
|
||||
return fmt.Errorf("vpp.lb.ipv6-src-address: %q is not a valid IPv6 address", s)
|
||||
}
|
||||
cfg.LB.IPv6SrcAddress = ip.To16()
|
||||
}
|
||||
|
||||
// sticky-buckets-per-core: default 65536, must be power of 2.
|
||||
if p := r.LB.StickyBucketsPerCore; p != nil {
|
||||
n := *p
|
||||
if n == 0 || n&(n-1) != 0 {
|
||||
return fmt.Errorf("vpp.lb.sticky-buckets-per-core: %d must be a power of 2", n)
|
||||
}
|
||||
cfg.LB.StickyBucketsPerCore = n
|
||||
} else {
|
||||
cfg.LB.StickyBucketsPerCore = 65536
|
||||
}
|
||||
|
||||
// flow-timeout: default 40s, must be 1-120s inclusive and a whole number of seconds.
|
||||
if s := r.LB.FlowTimeout; s != "" {
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("vpp.lb.flow-timeout: %w", err)
|
||||
}
|
||||
if d%time.Second != 0 {
|
||||
return fmt.Errorf("vpp.lb.flow-timeout: %s must be a whole number of seconds", d)
|
||||
}
|
||||
if d < time.Second || d > 120*time.Second {
|
||||
return fmt.Errorf("vpp.lb.flow-timeout: %s out of range [1s, 120s]", d)
|
||||
}
|
||||
cfg.LB.FlowTimeout = d
|
||||
} else {
|
||||
cfg.LB.FlowTimeout = 40 * time.Second
|
||||
}
|
||||
|
||||
// A missing src address is a hard error: VPP's GRE encap needs a source,
|
||||
// and every VIP we program uses GRE. Fail the config check so the
|
||||
// operator cannot start maglevd with a broken setup.
|
||||
if cfg.LB.IPv4SrcAddress == nil {
|
||||
return fmt.Errorf("vpp.lb.ipv4-src-address must be set; VPP GRE4 encap will fail for IPv4 VIPs")
|
||||
}
|
||||
if cfg.LB.IPv6SrcAddress == nil {
|
||||
return fmt.Errorf("vpp.lb.ipv6-src-address must be set; VPP GRE6 encap will fail for IPv6 VIPs")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertHealthCheck(r *rawHealthCheck) (HealthCheck, error) {
|
||||
h := HealthCheck{Type: r.Type, Port: r.Port}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ maglev:
|
||||
healthchecker:
|
||||
transition-history: 5
|
||||
netns: dataplane
|
||||
vpp:
|
||||
lb:
|
||||
ipv4-src-address: 10.0.0.1
|
||||
ipv6-src-address: 2001:db8::1
|
||||
healthchecks:
|
||||
http-check:
|
||||
type: http
|
||||
@@ -150,6 +154,10 @@ func TestValidConfig(t *testing.T) {
|
||||
func TestDefaults(t *testing.T) {
|
||||
raw := `
|
||||
maglev:
|
||||
vpp:
|
||||
lb:
|
||||
ipv4-src-address: 10.0.0.1
|
||||
ipv6-src-address: 2001:db8::1
|
||||
healthchecks:
|
||||
icmp:
|
||||
type: icmp
|
||||
@@ -196,6 +204,10 @@ func TestBackendNoHealthcheck(t *testing.T) {
|
||||
// A backend with no healthcheck reference is valid; probe is skipped.
|
||||
raw := `
|
||||
maglev:
|
||||
vpp:
|
||||
lb:
|
||||
ipv4-src-address: 10.0.0.1
|
||||
ipv6-src-address: 2001:db8::1
|
||||
healthchecks: {}
|
||||
backends:
|
||||
be:
|
||||
@@ -220,6 +232,10 @@ maglev:
|
||||
func TestOptionalIntervals(t *testing.T) {
|
||||
raw := `
|
||||
maglev:
|
||||
vpp:
|
||||
lb:
|
||||
ipv4-src-address: 10.0.0.1
|
||||
ipv6-src-address: 2001:db8::1
|
||||
healthchecks:
|
||||
icmp:
|
||||
type: icmp
|
||||
@@ -259,6 +275,10 @@ func TestValidationErrors(t *testing.T) {
|
||||
base := func(hcExtra, beExtra, feExtra string) string {
|
||||
return `
|
||||
maglev:
|
||||
vpp:
|
||||
lb:
|
||||
ipv4-src-address: 10.0.0.1
|
||||
ipv6-src-address: 2001:db8::1
|
||||
healthchecks:
|
||||
c:
|
||||
type: icmp
|
||||
@@ -294,6 +314,10 @@ maglev:
|
||||
name: "mixed backend address families in pool",
|
||||
yaml: `
|
||||
maglev:
|
||||
vpp:
|
||||
lb:
|
||||
ipv4-src-address: 10.0.0.1
|
||||
ipv6-src-address: 2001:db8::1
|
||||
healthchecks:
|
||||
c:
|
||||
type: icmp
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,8 @@ const (
|
||||
Maglev_CheckConfig_FullMethodName = "/maglev.Maglev/CheckConfig"
|
||||
Maglev_ReloadConfig_FullMethodName = "/maglev.Maglev/ReloadConfig"
|
||||
Maglev_GetVPPInfo_FullMethodName = "/maglev.Maglev/GetVPPInfo"
|
||||
Maglev_GetVPPLBState_FullMethodName = "/maglev.Maglev/GetVPPLBState"
|
||||
Maglev_SyncVPPLBState_FullMethodName = "/maglev.Maglev/SyncVPPLBState"
|
||||
)
|
||||
|
||||
// MaglevClient is the client API for Maglev service.
|
||||
@@ -57,6 +59,8 @@ type MaglevClient interface {
|
||||
CheckConfig(ctx context.Context, in *CheckConfigRequest, opts ...grpc.CallOption) (*CheckConfigResponse, error)
|
||||
ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error)
|
||||
GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, opts ...grpc.CallOption) (*VPPInfo, error)
|
||||
GetVPPLBState(ctx context.Context, in *GetVPPLBStateRequest, opts ...grpc.CallOption) (*VPPLBState, error)
|
||||
SyncVPPLBState(ctx context.Context, in *SyncVPPLBStateRequest, opts ...grpc.CallOption) (*SyncVPPLBStateResponse, error)
|
||||
}
|
||||
|
||||
type maglevClient struct {
|
||||
@@ -226,6 +230,26 @@ func (c *maglevClient) GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, op
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *maglevClient) GetVPPLBState(ctx context.Context, in *GetVPPLBStateRequest, opts ...grpc.CallOption) (*VPPLBState, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(VPPLBState)
|
||||
err := c.cc.Invoke(ctx, Maglev_GetVPPLBState_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *maglevClient) SyncVPPLBState(ctx context.Context, in *SyncVPPLBStateRequest, opts ...grpc.CallOption) (*SyncVPPLBStateResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SyncVPPLBStateResponse)
|
||||
err := c.cc.Invoke(ctx, Maglev_SyncVPPLBState_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MaglevServer is the server API for Maglev service.
|
||||
// All implementations must embed UnimplementedMaglevServer
|
||||
// for forward compatibility.
|
||||
@@ -247,6 +271,8 @@ type MaglevServer interface {
|
||||
CheckConfig(context.Context, *CheckConfigRequest) (*CheckConfigResponse, error)
|
||||
ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error)
|
||||
GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error)
|
||||
GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error)
|
||||
SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error)
|
||||
mustEmbedUnimplementedMaglevServer()
|
||||
}
|
||||
|
||||
@@ -302,6 +328,12 @@ func (UnimplementedMaglevServer) ReloadConfig(context.Context, *ReloadConfigRequ
|
||||
func (UnimplementedMaglevServer) GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetVPPInfo not implemented")
|
||||
}
|
||||
func (UnimplementedMaglevServer) GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetVPPLBState not implemented")
|
||||
}
|
||||
func (UnimplementedMaglevServer) SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SyncVPPLBState not implemented")
|
||||
}
|
||||
func (UnimplementedMaglevServer) mustEmbedUnimplementedMaglevServer() {}
|
||||
func (UnimplementedMaglevServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -586,6 +618,42 @@ func _Maglev_GetVPPInfo_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Maglev_GetVPPLBState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetVPPLBStateRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MaglevServer).GetVPPLBState(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Maglev_GetVPPLBState_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MaglevServer).GetVPPLBState(ctx, req.(*GetVPPLBStateRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Maglev_SyncVPPLBState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SyncVPPLBStateRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MaglevServer).SyncVPPLBState(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Maglev_SyncVPPLBState_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MaglevServer).SyncVPPLBState(ctx, req.(*SyncVPPLBStateRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Maglev_ServiceDesc is the grpc.ServiceDesc for Maglev service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -649,6 +717,14 @@ var Maglev_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetVPPInfo",
|
||||
Handler: _Maglev_GetVPPInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetVPPLBState",
|
||||
Handler: _Maglev_GetVPPLBState_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SyncVPPLBState",
|
||||
Handler: _Maglev_SyncVPPLBState_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ package grpcapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
|
||||
@@ -255,6 +256,13 @@ func (s *Server) doReloadConfig() *ReloadConfigResponse {
|
||||
ReloadError: err.Error(),
|
||||
}
|
||||
}
|
||||
// Push new global LB conf to VPP if anything changed. SetLBConf is a
|
||||
// no-op when VPP isn't connected or when the values are unchanged.
|
||||
if s.vppClient != nil {
|
||||
if err := s.vppClient.SetLBConf(newCfg); err != nil {
|
||||
slog.Warn("vpp-lb-conf-set-failed", "err", err)
|
||||
}
|
||||
}
|
||||
slog.Info("config-reload-done", "frontends", len(newCfg.Frontends))
|
||||
return &ReloadConfigResponse{Ok: true}
|
||||
}
|
||||
@@ -282,6 +290,84 @@ func (s *Server) GetVPPInfo(_ context.Context, _ *GetVPPInfoRequest) (*VPPInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetVPPLBState returns a snapshot of the VPP load-balancer plugin state.
|
||||
func (s *Server) GetVPPLBState(_ context.Context, _ *GetVPPLBStateRequest) (*VPPLBState, error) {
|
||||
if s.vppClient == nil {
|
||||
return nil, status.Error(codes.Unavailable, "VPP integration is disabled")
|
||||
}
|
||||
state, err := s.vppClient.GetLBStateAll()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unavailable, "%v", err)
|
||||
}
|
||||
return lbStateToProto(state), nil
|
||||
}
|
||||
|
||||
// SyncVPPLBState runs the LB reconciler. With frontend_name unset it does a
|
||||
// full sync (SyncLBStateAll), which may remove stale VIPs. With frontend_name
|
||||
// set it does a single-VIP sync (SyncLBStateVIP) that only adds/updates.
|
||||
func (s *Server) SyncVPPLBState(_ context.Context, req *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) {
|
||||
if s.vppClient == nil {
|
||||
return nil, status.Error(codes.Unavailable, "VPP integration is disabled")
|
||||
}
|
||||
cfg := s.checker.Config()
|
||||
if req.FrontendName != nil && *req.FrontendName != "" {
|
||||
if err := s.vppClient.SyncLBStateVIP(cfg, *req.FrontendName); err != nil {
|
||||
if errors.Is(err, vpp.ErrFrontendNotFound) {
|
||||
return nil, status.Errorf(codes.NotFound, "%v", err)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unavailable, "%v", err)
|
||||
}
|
||||
return &SyncVPPLBStateResponse{}, nil
|
||||
}
|
||||
if err := s.vppClient.SyncLBStateAll(cfg); err != nil {
|
||||
return nil, status.Errorf(codes.Unavailable, "%v", err)
|
||||
}
|
||||
return &SyncVPPLBStateResponse{}, nil
|
||||
}
|
||||
|
||||
// lbStateToProto converts the vpp package's LBState into the proto message.
|
||||
func lbStateToProto(s *vpp.LBState) *VPPLBState {
|
||||
out := &VPPLBState{
|
||||
Conf: &VPPLBConf{
|
||||
Ip4SrcAddress: ipStringOrEmpty(s.Conf.IP4SrcAddress),
|
||||
Ip6SrcAddress: ipStringOrEmpty(s.Conf.IP6SrcAddress),
|
||||
StickyBucketsPerCore: s.Conf.StickyBucketsPerCore,
|
||||
FlowTimeout: s.Conf.FlowTimeout,
|
||||
},
|
||||
}
|
||||
for _, v := range s.VIPs {
|
||||
pv := &VPPLBVIP{
|
||||
Prefix: v.Prefix.String(),
|
||||
Protocol: uint32(v.Protocol),
|
||||
Port: uint32(v.Port),
|
||||
Encap: v.Encap,
|
||||
FlowTableLength: uint32(v.FlowTableLength),
|
||||
}
|
||||
for _, a := range v.ASes {
|
||||
var ts int64
|
||||
if !a.InUseSince.IsZero() {
|
||||
ts = a.InUseSince.UnixNano()
|
||||
}
|
||||
pv.ApplicationServers = append(pv.ApplicationServers, &VPPLBAS{
|
||||
Address: a.Address.String(),
|
||||
Weight: uint32(a.Weight),
|
||||
Flags: uint32(a.Flags),
|
||||
NumBuckets: a.NumBuckets,
|
||||
InUseSinceNs: ts,
|
||||
})
|
||||
}
|
||||
out.Vips = append(out.Vips, pv)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ipStringOrEmpty(ip net.IP) string {
|
||||
if len(ip) == 0 || ip.IsUnspecified() {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// ---- conversion helpers ----------------------------------------------------
|
||||
|
||||
func frontendToProto(name string, fe config.Frontend) *FrontendInfo {
|
||||
|
||||
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/socketclient"
|
||||
"go.fd.io/govpp/adapter/statsclient"
|
||||
"go.fd.io/govpp/api"
|
||||
"go.fd.io/govpp/binapi/vpe"
|
||||
"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 pingInterval = 10 * time.Second
|
||||
const defaultLBSyncInterval = 30 * time.Second
|
||||
|
||||
// Info holds VPP version and connection metadata, populated on connect.
|
||||
type Info struct {
|
||||
@@ -44,6 +54,17 @@ type Client struct {
|
||||
statsConn *core.StatsConnection
|
||||
statsClient adapter.StatsAPI // raw adapter for DumpStats
|
||||
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.
|
||||
@@ -77,8 +98,43 @@ func (c *Client) Run(ctx context.Context) {
|
||||
"pid", c.info.PID,
|
||||
"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.
|
||||
c.monitor(ctx)
|
||||
connCancel()
|
||||
|
||||
// If ctx is done we're shutting down; otherwise VPP dropped and we retry.
|
||||
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.
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.mu.Lock()
|
||||
@@ -96,25 +195,6 @@ func (c *Client) IsConnected() bool {
|
||||
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
|
||||
// if VPP is not connected.
|
||||
func (c *Client) GetInfo() (Info, error) {
|
||||
@@ -160,6 +240,7 @@ func (c *Client) disconnect() {
|
||||
c.statsConn = nil
|
||||
c.statsClient = nil
|
||||
c.info = Info{}
|
||||
c.lastLBConf = nil // force re-push of lb_conf on reconnect
|
||||
c.mu.Unlock()
|
||||
|
||||
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.
|
||||
func (c *Client) ping() bool {
|
||||
ch, err := c.NewAPIChannel()
|
||||
ch, err := c.apiChannel()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -204,7 +285,7 @@ func (c *Client) ping() bool {
|
||||
func (c *Client) fetchInfo() Info {
|
||||
info := Info{ConnectedSince: time.Now()}
|
||||
|
||||
ch, err := c.NewAPIChannel()
|
||||
ch, err := c.apiChannel()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user