diff --git a/api/agent/v1beta1/agent_types.go b/api/agent/v1beta1/agent_types.go index aa21daff..5ff946ca 100644 --- a/api/agent/v1beta1/agent_types.go +++ b/api/agent/v1beta1/agent_types.go @@ -75,6 +75,7 @@ type AgentSpecConfig struct { ESLAGESIPrefix string `json:"eslagESIPrefix,omitempty"` DefaultMaxPathsEBGP uint32 `json:"defaultMaxPathsEBGP,omitempty"` MCLAGSessionSubnet string `json:"mclagSessionSubnet,omitempty"` + GatewayASN uint32 `json:"gatewayASN,omitempty"` } type AgentSpecConfigCollapsedCore struct{} diff --git a/api/meta/types.go b/api/meta/types.go index f0f12a5c..625ece2c 100644 --- a/api/meta/types.go +++ b/api/meta/types.go @@ -83,6 +83,7 @@ type FabricConfig struct { DefaultMaxPathsEBGP uint32 `json:"defaultMaxPathsEBGP,omitempty"` AllowExtraSwitchProfiles bool `json:"allowExtraSwitchProfiles,omitempty"` MCLAGSessionSubnet string `json:"mclagSessionSubnet,omitempty"` + GatewayASN uint32 `json:"gatewayASN,omitempty"` // Temporarily assuming that all GWs are in the same AS reservedSubnets []*net.IPNet } @@ -253,6 +254,10 @@ func (cfg *FabricConfig) Init() (*FabricConfig, error) { return nil, errors.Errorf("config: defaultMaxPathsEBGP is required") } + if cfg.GatewayASN == 0 { + return nil, errors.Errorf("config: gatewayASN is required") + } + // TODO validate format of all fields // slog.Debug("Loaded Fabric config", "data", spew.Sdump(cfg)) diff --git a/config/crd/bases/agent.githedgehog.com_agents.yaml b/config/crd/bases/agent.githedgehog.com_agents.yaml index f48961f7..6d889548 100644 --- a/config/crd/bases/agent.githedgehog.com_agents.yaml +++ b/config/crd/bases/agent.githedgehog.com_agents.yaml @@ -258,6 +258,9 @@ spec: type: string fabricMTU: type: integer + gatewayASN: + format: int32 + type: integer mclagSessionSubnet: type: string serverFacingMTUOffset: diff --git a/pkg/agent/dozer/bcm/plan.go b/pkg/agent/dozer/bcm/plan.go index b3925d37..e79d9800 100644 --- a/pkg/agent/dozer/bcm/plan.go +++ b/pkg/agent/dozer/bcm/plan.go @@ -147,6 +147,11 @@ func (p *BroadcomProcessor) PlanDesiredState(_ context.Context, agent *agentapi. return nil, errors.Wrap(err, "failed to plan fabric connections") } + err = planGatewayConnections(agent, spec) + if err != nil { + return nil, errors.Wrap(err, "failed to plan gateway connections") + } + err = planVPCLoopbacks(agent, spec) if err != nil { return nil, errors.Wrap(err, "failed to plan VPC loopbacks") @@ -429,6 +434,80 @@ func planFabricConnections(agent *agentapi.Agent, spec *dozer.Spec) error { return nil } +func planGatewayConnections(agent *agentapi.Agent, spec *dozer.Spec) error { + if !agent.IsSpineLeaf() { + return nil + } + + for connName, conn := range agent.Spec.Connections { + if conn.Gateway == nil { + continue + } + + if agent.Spec.Config.GatewayASN == 0 { + return errors.Errorf("gateway ASN not set") + } + + for _, link := range conn.Gateway.Links { + port := "" + ipStr := "" + remote := "" + // peer := "" + peerIP := "" + if link.Spine.DeviceName() == agent.Name { + port = link.Spine.LocalPortName() + ipStr = link.Spine.IP + remote = link.Gateway.Port + // peer = link.Gateway.DeviceName() + peerIP = link.Gateway.IP + } else { + continue + } + + if ipStr == "" { + return errors.Errorf("no IP found for gateway conn %s", connName) + } + + ip, ipNet, err := net.ParseCIDR(ipStr) + if err != nil { + return errors.Wrapf(err, "failed to parse gateway conn ip %s", ipStr) + } + ipPrefixLen, _ := ipNet.Mask.Size() + + spec.Interfaces[port] = &dozer.SpecInterface{ + Enabled: pointer.To(true), + Description: pointer.To(fmt.Sprintf("Gateway %s %s", remote, connName)), + Speed: getPortSpeed(agent, port), + Subinterfaces: map[uint32]*dozer.SpecSubinterface{ + 0: { + IPs: map[string]*dozer.SpecInterfaceIP{ + ip.String(): { + PrefixLen: pointer.To(uint8(ipPrefixLen)), //nolint:gosec + }, + }, + }, + }, + } + + ip, _, err = net.ParseCIDR(peerIP) + if err != nil { + return errors.Wrapf(err, "failed to parse gateway conn peer ip %s", peerIP) + } + + spec.VRFs[VRFDefault].BGP.Neighbors[ip.String()] = &dozer.SpecVRFBGPNeighbor{ + Enabled: pointer.To(true), + Description: pointer.To(fmt.Sprintf("Gateway %s %s", remote, connName)), + RemoteAS: pointer.To(agent.Spec.Config.GatewayASN), // TODO load peer GW and get ASN from it + IPv4Unicast: pointer.To(true), + L2VPNEVPN: pointer.To(true), + L2VPNEVPNAllowOwnAS: pointer.To(true), + } + } + } + + return nil +} + func planVPCLoopbacks(agent *agentapi.Agent, spec *dozer.Spec) error { //nolint:unparam for connName, conn := range agent.Spec.Connections { if conn.VPCLoopback == nil { diff --git a/pkg/ctrl/agent/agent_ctrl.go b/pkg/ctrl/agent/agent_ctrl.go index 89cae6af..bc0c0627 100644 --- a/pkg/ctrl/agent/agent_ctrl.go +++ b/pkg/ctrl/agent/agent_ctrl.go @@ -684,6 +684,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu ESLAGESIPrefix: r.Cfg.ESLAGESIPrefix, DefaultMaxPathsEBGP: r.Cfg.DefaultMaxPathsEBGP, MCLAGSessionSubnet: r.Cfg.MCLAGSessionSubnet, + GatewayASN: r.Cfg.GatewayASN, } if r.Cfg.FabricMode == meta.FabricModeCollapsedCore { agent.Spec.Config.CollapsedCore = &agentapi.AgentSpecConfigCollapsedCore{}