diff --git a/api/vpc/v1beta1/vpc_types.go b/api/vpc/v1beta1/vpc_types.go index 4b71ec2e0..93457c871 100644 --- a/api/vpc/v1beta1/vpc_types.go +++ b/api/vpc/v1beta1/vpc_types.go @@ -91,6 +91,8 @@ type VPCSubnet struct { Isolated *bool `json:"isolated,omitempty"` // Restricted is the flag to enable restricted mode for the subnet which means no access between hosts within the subnet itself Restricted *bool `json:"restricted,omitempty"` + // HostBGP is the flag to set this Subnet as dedicated to BGP speaking hosts advertising their VIPs within the subnet's IP range + HostBGP bool `json:"hostBGP,omitempty"` } // VPCDHCP defines the on-demand DHCP configuration for the subnet @@ -272,7 +274,7 @@ func (vpc *VPC) Default() { continue } - if subnet.Gateway == "" { + if subnet.Gateway == "" && !subnet.HostBGP { subnet.Gateway = cidr.Gateway.String() } @@ -383,6 +385,7 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me subnets := []*net.IPNet{} vlans := map[uint16]bool{} + hostBGPSubnets := 0 for subnetName, subnetCfg := range vpc.Spec.Subnets { if subnetCfg.Subnet == "" { return nil, errors.Errorf("subnet %s: missing subnet", subnetName) @@ -405,19 +408,30 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me } } - if subnetCfg.Gateway == "" { - return nil, errors.Errorf("subnet %s: gateway is required", subnetName) - } + var gateway net.IP + if subnetCfg.HostBGP { + hostBGPSubnets++ + if subnetCfg.VLAN != 0 { + return nil, errors.Errorf("subnet %s: vlan should not be set for hostBGP subnets", subnetName) + } + if subnetCfg.DHCP.Enable { + return nil, errors.Errorf("subnet %s: dhcp should not be enabled for hostBGP subnets", subnetName) + } + } else { + if subnetCfg.Gateway == "" { + return nil, errors.Errorf("subnet %s: gateway is required", subnetName) + } - gateway := net.ParseIP(subnetCfg.Gateway) - if !ipNet.Contains(gateway) { - return nil, errors.Errorf("subnet %s: gateway %s is not in the subnet", subnetName, subnetCfg.Gateway) - } + gateway = net.ParseIP(subnetCfg.Gateway) + if !ipNet.Contains(gateway) { + return nil, errors.Errorf("subnet %s: gateway %s is not in the subnet", subnetName, subnetCfg.Gateway) + } - if subnetCfg.VLAN == 0 { - return nil, errors.Errorf("subnet %s: vlan is required", subnetName) + if subnetCfg.VLAN == 0 { + return nil, errors.Errorf("subnet %s: vlan is required", subnetName) + } + vlans[subnetCfg.VLAN] = true } - vlans[subnetCfg.VLAN] = true subnets = append(subnets, ipNet) @@ -597,7 +611,7 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me } } - if len(vlans) != len(vpc.Spec.Subnets) { + if len(vlans) != len(vpc.Spec.Subnets)-hostBGPSubnets { return nil, errors.Errorf("duplicate subnet VLANs") } @@ -690,7 +704,7 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me return nil, errors.Errorf("vpc subnet %s (%s) doesn't belong to the IPv4Namespace %s", subnetName, subnetCfg.Subnet, vpc.Spec.IPv4Namespace) } - if !vlanNs.Spec.Contains(subnetCfg.VLAN) { + if !subnetCfg.HostBGP && !vlanNs.Spec.Contains(subnetCfg.VLAN) { return nil, errors.Errorf("vpc subnet %s (%s) vlan %d doesn't belong to the VLANNamespace %s", subnetName, subnetCfg.Subnet, subnetCfg.VLAN, vpc.Spec.VLANNamespace) } } diff --git a/cmd/hhfctl/main.go b/cmd/hhfctl/main.go index ccdce4761..ecac5f24b 100644 --- a/cmd/hhfctl/main.go +++ b/cmd/hhfctl/main.go @@ -171,9 +171,8 @@ func main() { Required: true, }, &cli.StringFlag{ - Name: "vlan", - Usage: "vlan", - Required: true, + Name: "vlan", + Usage: "vlan", }, &cli.BoolFlag{ Name: "dhcp", @@ -208,6 +207,10 @@ func main() { Name: "dhcp-advertised-routes", Usage: "custom routes to advertise in dhcp, in the format prefix-gateway, e.g. 8.8.8.0/24-192.168.1.1", }, + &cli.BoolFlag{ + Name: "host-bgp", + Usage: "mark the subnet as dedicated to BGP speakers", + }, printYamlFlag, }, Before: func(_ *cli.Context) error { @@ -244,7 +247,8 @@ func main() { AdvertisedRoutes: advertisedRoutes, }, }, - Mode: vpcapi.VPCMode(cCtx.String("vpc-mode")), + Mode: vpcapi.VPCMode(cCtx.String("vpc-mode")), + HostBGP: cCtx.Bool("host-bgp"), }), "failed to create vpc") }, }, diff --git a/config/crd/bases/agent.githedgehog.com_agents.yaml b/config/crd/bases/agent.githedgehog.com_agents.yaml index f4d413b32..bbdb9be35 100644 --- a/config/crd/bases/agent.githedgehog.com_agents.yaml +++ b/config/crd/bases/agent.githedgehog.com_agents.yaml @@ -1781,6 +1781,11 @@ spec: specified, the first IP (e.g. 10.0.0.1) in the subnet is used as the gateway type: string + hostBGP: + description: HostBGP is the flag to set this Subnet as + dedicated to BGP speaking hosts advertising their VIPs + within the subnet's IP range + type: boolean isolated: description: Isolated is the flag to enable isolated mode for the subnet which means no access to and from the diff --git a/config/crd/bases/vpc.githedgehog.com_vpcs.yaml b/config/crd/bases/vpc.githedgehog.com_vpcs.yaml index e440aab35..1c4440006 100644 --- a/config/crd/bases/vpc.githedgehog.com_vpcs.yaml +++ b/config/crd/bases/vpc.githedgehog.com_vpcs.yaml @@ -210,6 +210,11 @@ spec: the first IP (e.g. 10.0.0.1) in the subnet is used as the gateway type: string + hostBGP: + description: HostBGP is the flag to set this Subnet as dedicated + to BGP speaking hosts advertising their VIPs within the subnet's + IP range + type: boolean isolated: description: Isolated is the flag to enable isolated mode for the subnet which means no access to and from the other subnets diff --git a/docs/api.md b/docs/api.md index cbd817485..9aeaaa232 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1436,6 +1436,7 @@ _Appears in:_ | `vlan` _integer_ | VLAN is the VLAN ID for the subnet, should belong to the VLANNamespace and be unique within the namespace | | | | `isolated` _boolean_ | Isolated is the flag to enable isolated mode for the subnet which means no access to and from the other subnets within the VPC | | | | `restricted` _boolean_ | Restricted is the flag to enable restricted mode for the subnet which means no access between hosts within the subnet itself | | | +| `hostBGP` _boolean_ | HostBGP is the flag to set this Subnet as dedicated to BGP speaking hosts advertising their VIPs within the subnet's IP range | | | diff --git a/pkg/agent/dozer/bcm/enforcer.go b/pkg/agent/dozer/bcm/enforcer.go index 0358b5998..31f090339 100644 --- a/pkg/agent/dozer/bcm/enforcer.go +++ b/pkg/agent/dozer/bcm/enforcer.go @@ -99,11 +99,13 @@ const ( ActionWeightInterfaceSubinterfaceIPsDelete ActionWeightVRFAttachedHostDelete + ActionWeightInterfaceSubinterfaceIPv6Delete ActionWeightVRFInterfaceDelete ActionWeightACLInterfaceDelete ActionWeightInterfaceSubinterfaceDelete ActionWeightInterfaceSubinterfaceUpdate ActionWeightVRFInterfaceUpdate + ActionWeightInterfaceSubinterfaceIPv6Update ActionWeightVRFAttachedHostUpdate ActionWeightInterfaceSubinterfaceIPsUpdate diff --git a/pkg/agent/dozer/bcm/plan.go b/pkg/agent/dozer/bcm/plan.go index de620875b..966405661 100644 --- a/pkg/agent/dozer/bcm/plan.go +++ b/pkg/agent/dozer/bcm/plan.go @@ -1778,14 +1778,16 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { return errors.Errorf("VPC %s subnet %s not found", vpcName, subnetName) } - switch vpc.Mode { - case vpcapi.VPCModeL2VNI, vpcapi.VPCModeL3VNI: - if err := planVNIVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil { - return errors.Wrapf(err, "failed to plan VPC %s subnet %s", vpcName, subnetName) - } - case vpcapi.VPCModeL3Flat: - if err := planL3FlatVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil { - return errors.Wrapf(err, "failed to plan L3 VPC %s subnet %s", vpcName, subnetName) + if !subnet.HostBGP { + switch vpc.Mode { + case vpcapi.VPCModeL2VNI, vpcapi.VPCModeL3VNI: + if err := planVNIVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil { + return errors.Wrapf(err, "failed to plan VPC %s subnet %s", vpcName, subnetName) + } + case vpcapi.VPCModeL3Flat: + if err := planL3FlatVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil { + return errors.Wrapf(err, "failed to plan L3 VPC %s subnet %s", vpcName, subnetName) + } } } @@ -1842,13 +1844,19 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { ifaces = append(ifaces, conn.Unbundled.Link.Switch.LocalPortName()) } - for _, iface := range ifaces { - if attach.NativeVLAN { - spec.Interfaces[iface].AccessVLAN = pointer.To(subnet.VLAN) - } else { - vlanStr := fmt.Sprintf("%d", subnet.VLAN) - if !slices.Contains(spec.Interfaces[iface].TrunkVLANs, vlanStr) { - spec.Interfaces[iface].TrunkVLANs = append(spec.Interfaces[iface].TrunkVLANs, vlanStr) + if subnet.HostBGP { + if err := planHostBGPSubnet(agent, spec, vpcName, vpc, subnetName, subnet, ifaces); err != nil { + return errors.Wrapf(err, "failed to plan HostBGP VPC %s subnet %s", vpcName, subnetName) + } + } else { + for _, iface := range ifaces { + if attach.NativeVLAN { + spec.Interfaces[iface].AccessVLAN = pointer.To(subnet.VLAN) + } else { + vlanStr := fmt.Sprintf("%d", subnet.VLAN) + if !slices.Contains(spec.Interfaces[iface].TrunkVLANs, vlanStr) { + spec.Interfaces[iface].TrunkVLANs = append(spec.Interfaces[iface].TrunkVLANs, vlanStr) + } } } } @@ -1877,6 +1885,11 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { return errors.Errorf("VPC %s subnet %s not found", vpcName, subnetName) } + // no VLAN interface to configure on HostBGP subnets + if subnet.HostBGP { + continue + } + switch vpc.Mode { case vpcapi.VPCModeL2VNI, vpcapi.VPCModeL3VNI: if err := planVNIVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil { @@ -1931,14 +1944,25 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { if acl, ok := spec.ACLs[aclName]; ok { if len(acl.Entries) == 1 { delete(spec.ACLs, aclName) - - subnetIface := vlanName(subnet.VLAN) - if aclIface, ok := spec.ACLInterfaces[subnetIface]; ok { - if aclIface.Ingress != nil && *aclIface.Ingress == aclName { - aclIface.Ingress = nil - - if aclIface.Egress == nil { - delete(spec.ACLInterfaces, subnetIface) + if subnet.HostBGP { + // FIXME: we don't havean easy way to get the interface(s) name here + for ifaceName, aclIface := range spec.ACLInterfaces { + if aclIface.Ingress != nil && *aclIface.Ingress == aclName { + aclIface.Ingress = nil + if aclIface.Egress == nil { + delete(spec.ACLInterfaces, ifaceName) + } + } + } + } else { + subnetIface := vlanName(subnet.VLAN) + if aclIface, ok := spec.ACLInterfaces[subnetIface]; ok { + if aclIface.Ingress != nil && *aclIface.Ingress == aclName { + aclIface.Ingress = nil + + if aclIface.Egress == nil { + delete(spec.ACLInterfaces, subnetIface) + } } } } @@ -2495,6 +2519,87 @@ func addL3FlatVPCFilteringACLEntryiesForVPC(agent *agentapi.Agent, spec *dozer.S return nil } +func planHostBGPSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName string, vpc vpcapi.VPCSpec, subnetName string, subnet *vpcapi.VPCSubnet, ifaceNames []string) error { + vrfName := vpcVrfName(vpcName) + _, err := iputil.ParseCIDR(subnet.Subnet) + if err != nil { + return errors.Wrapf(err, "failed to parse hostBGP subnet %s for VPC %s", subnet.Subnet, vpcName) + } + + // create prefix list matching /32s in this subnet prefix + plName := vpcSubnetVIPsOnlyPrefixListName(vpcName, subnetName) + spec.PrefixLists[plName] = &dozer.SpecPrefixList{ + Prefixes: map[uint32]*dozer.SpecPrefixListEntry{ + 10: { + Prefix: dozer.SpecPrefixListPrefix{ + Prefix: subnet.Subnet, + Ge: 32, + Le: 32, + }, + Action: dozer.SpecPrefixListActionPermit, + }, + }, + } + + // create routemap filtering anything that is not the prefix list above + rmName := vpcSubnetVIPsOnlyRouteMapName(vpcName, subnetName) + spec.RouteMaps[rmName] = &dozer.SpecRouteMap{ + Statements: map[string]*dozer.SpecRouteMapStatement{ + "10": { + Conditions: dozer.SpecRouteMapConditions{MatchPrefixList: pointer.To(plName)}, + Result: dozer.SpecRouteMapResultAccept, + }, + }, + } + + vpcFilteringACL := vpcFilteringAccessListName(vpcName, subnetName) + spec.ACLs[vpcFilteringACL], err = buildVNIVPCFilteringACL(agent, vpcName, vpc, subnetName, subnet) + if err != nil { + return errors.Wrapf(err, "failed to plan VPC filtering ACL for VPC %s hostBGP subnet %s", vpcName, subnetName) + } + + // for each of the attachment interfaces on the switch side, enslave them to the VPC VRF and enable IPv6 for BGP unnumbered + // then add an unnumbered BGP neighbor on that interface in the VPC VRF instance + // finally add the ACL to handle restricted subnets (this will be removed if it's empty) + for _, iface := range ifaceNames { + spec.VRFs[vrfName].Interfaces[iface] = &dozer.SpecVRFInterface{} + if spec.Interfaces[iface].Subinterfaces == nil { + spec.Interfaces[iface].Subinterfaces = map[uint32]*dozer.SpecSubinterface{ + 0: {}, + } + } + subIf0, ok := spec.Interfaces[iface].Subinterfaces[0] + if !ok { + spec.Interfaces[iface].Subinterfaces[0] = &dozer.SpecSubinterface{} + subIf0 = spec.Interfaces[iface].Subinterfaces[0] + } + subIf0.IPv6 = &dozer.SpecInterfaceIPv6{ + Enabled: pointer.To(true), + } + + if spec.VRFs[vrfName].BGP == nil { + return errors.Errorf("VRF %s BGP not found when planning hostBGP for VPC %s subnet %s", vrfName, vpcName, subnetName) + } + if spec.VRFs[vrfName].BGP.Neighbors == nil { + spec.VRFs[vrfName].BGP.Neighbors = map[string]*dozer.SpecVRFBGPNeighbor{} + } + spec.VRFs[vrfName].BGP.Neighbors[iface] = &dozer.SpecVRFBGPNeighbor{ + Enabled: pointer.To(true), + Description: pointer.To(fmt.Sprintf("HostBGP unnumbered %s", iface)), + PeerType: pointer.To(string(dozer.SpecVRFBGPNeighborPeerTypeExternal)), + ExtendedNexthop: pointer.To(true), + IPv4Unicast: pointer.To(true), + IPv4UnicastImportPolicies: []string{rmName}, + } + + spec.ACLInterfaces[iface] = &dozer.SpecACLInterface{ + Ingress: pointer.To(vpcFilteringACL), + } + } + + return nil +} + func planVNIVPCSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName string, vpc vpcapi.VPCSpec, subnetName string, subnet *vpcapi.VPCSubnet) error { vrfName := vpcVrfName(vpcName) @@ -3239,6 +3344,14 @@ func vpcFilteringAccessListName(vpc string, subnet string) string { return fmt.Sprintf("vpc-filtering--%s--%s", vpc, subnet) } +func vpcSubnetVIPsOnlyPrefixListName(vpc string, subnet string) string { + return fmt.Sprintf("vips-only--%s--%s", vpc, subnet) +} + +func vpcSubnetVIPsOnlyRouteMapName(vpc string, subnet string) string { + return fmt.Sprintf("vips-only--%s--%s", vpc, subnet) +} + func communityForVPC(agent *agentapi.Agent, vpc string) (string, error) { baseParts := strings.Split(agent.Spec.Config.BaseVPCCommunity, ":") if len(baseParts) != 2 { @@ -3426,6 +3539,25 @@ func translatePortNames(agent *agentapi.Agent, spec *dozer.Spec) error { } vrf.Interfaces = newIfaces + if vrf.BGP != nil { + newBGPNeighbors := map[string]*dozer.SpecVRFBGPNeighbor{} + for name, neighbor := range vrf.BGP.Neighbors { + newName := name + if isHedgehogPortName(name) { + newName, err = getNOSPortName(ports, name) + if err != nil { + return errors.Wrapf(err, "failed to translate port name %s for BGP neighbor in vrf %s", name, vrfName) + } + if neighbor.Description != nil { + neighbor.Description = pointer.To(strings.ReplaceAll(*neighbor.Description, name, newName)) + } + } + + newBGPNeighbors[newName] = neighbor + } + vrf.BGP.Neighbors = newBGPNeighbors + } + for routeName, route := range vrf.StaticRoutes { for idx, nextHop := range route.NextHops { if nextHop.Interface == nil { @@ -3449,6 +3581,8 @@ func translatePortNames(agent *agentapi.Agent, spec *dozer.Spec) error { } } + // BGP Unnumbered + return nil } diff --git a/pkg/agent/dozer/bcm/spec_interface.go b/pkg/agent/dozer/bcm/spec_interface.go index c5272ae39..51965a077 100644 --- a/pkg/agent/dozer/bcm/spec_interface.go +++ b/pkg/agent/dozer/bcm/spec_interface.go @@ -237,6 +237,12 @@ var specInterfaceSubinterfaceEnforcer = &DefaultValueEnforcer[uint32, *dozer.Spe if err := specInterfaceSubinterfaceBaseEnforcer.Handle(basePath, idx, actual, desired, actions); err != nil { return errors.Wrap(err, "failed to handle subinterface base") } + ipv6Path := basePath + "/ipv6/config/enabled" + actualV6, desiredV6 := ValueOrNil(actual, desired, + func(value *dozer.SpecSubinterface) *dozer.SpecInterfaceIPv6 { return value.IPv6 }) + if err := specInterfaceSubinterfaceIPv6Enforcer.Handle(ipv6Path, idx, actualV6, desiredV6, actions); err != nil { + return errors.Wrap(err, "failed to handle subinterface ipv6") + } actualIPs, desiredIPs := ValueOrNil(actual, desired, func(value *dozer.SpecSubinterface) map[string]*dozer.SpecInterfaceIP { return value.IPs }) @@ -248,6 +254,21 @@ var specInterfaceSubinterfaceEnforcer = &DefaultValueEnforcer[uint32, *dozer.Spe }, } +var specInterfaceSubinterfaceIPv6Enforcer = &DefaultValueEnforcer[uint32, *dozer.SpecInterfaceIPv6]{ + Summary: "Subinterface %d IPv6 Enable", + NoReplace: true, + UpdateWeight: ActionWeightInterfaceSubinterfaceIPv6Update, + DeleteWeight: ActionWeightInterfaceSubinterfaceIPv6Delete, + Marshal: func(idx uint32, value *dozer.SpecInterfaceIPv6) (ygot.ValidatedGoStruct, error) { + cfg := &oc.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv6_Config{} + if value != nil && value.Enabled != nil { + cfg.Enabled = pointer.To(*value.Enabled) + } + + return cfg, nil + }, +} + var specInterfaceSubinterfaceBaseEnforcer = &DefaultValueEnforcer[uint32, *dozer.SpecSubinterface]{ Summary: "Subinterface Base %d", NoReplace: true, // TODO check if it'll work correctly @@ -612,6 +633,13 @@ func unmarshalOCInterfaces(agent *agentapi.Agent, ocVal *oc.OpenconfigInterfaces } } + // only set this if it exists and it is true + if sub.Ipv6 != nil && sub.Ipv6.Config != nil && sub.Ipv6.Config.Enabled != nil && *sub.Ipv6.Config.Enabled { + subIface.IPv6 = &dozer.SpecInterfaceIPv6{ + Enabled: pointer.To(true), + } + } + iface.Subinterfaces[id] = subIface } } diff --git a/pkg/agent/dozer/bcm/spec_vrf.go b/pkg/agent/dozer/bcm/spec_vrf.go index ef36dfd28..d36715a7b 100644 --- a/pkg/agent/dozer/bcm/spec_vrf.go +++ b/pkg/agent/dozer/bcm/spec_vrf.go @@ -439,6 +439,7 @@ var specVRFBGPNeighborEnforcer = &DefaultValueEnforcer[string, *dozer.SpecVRFBGP PeerAs: remoteAS, PeerType: peerType, DisableEbgpConnectedRouteCheck: value.DisableConnectedCheck, + CapabilityExtendedNexthop: value.ExtendedNexthop, }, AfiSafis: &oc.OpenconfigNetworkInstance_NetworkInstances_NetworkInstance_Protocols_Protocol_Bgp_Neighbors_Neighbor_AfiSafis{ AfiSafi: map[oc.E_OpenconfigBgpTypes_AFI_SAFI_TYPE]*oc.OpenconfigNetworkInstance_NetworkInstances_NetworkInstance_Protocols_Protocol_Bgp_Neighbors_Neighbor_AfiSafis_AfiSafi{ //nolint:exhaustive,nolintlint @@ -890,6 +891,7 @@ func unmarshalOCVRFs(ocVal *oc.OpenconfigNetworkInstance_NetworkInstances) (map[ L2VPNEVPNAllowOwnAS: l2VPNEVPNAllowOwnAS, BFDProfile: bfdProfile, DisableConnectedCheck: neighbor.Config.DisableEbgpConnectedRouteCheck, + ExtendedNexthop: neighbor.Config.CapabilityExtendedNexthop, } if neighbor.Transport != nil && neighbor.Transport.Config != nil { bgp.Neighbors[neighborName].UpdateSource = neighbor.Transport.Config.LocalAddress diff --git a/pkg/agent/dozer/dozer.go b/pkg/agent/dozer/dozer.go index b8d055bc0..3880ba5f6 100644 --- a/pkg/agent/dozer/dozer.go +++ b/pkg/agent/dozer/dozer.go @@ -131,10 +131,15 @@ type SpecInterfaceIP struct { Secondary *bool `json:"secondary,omitempty"` } +type SpecInterfaceIPv6 struct { + Enabled *bool `json:"ipv6Enabled,omitempty"` +} + type SpecSubinterface struct { VLAN *uint16 `json:"vlan,omitempty"` IPs map[string]*SpecInterfaceIP `json:"ips,omitempty"` AnycastGateways []string `json:"anycastGateways,omitempty"` + IPv6 *SpecInterfaceIPv6 `json:"ipv6,omitempty"` } type SpecMCLAGDomain struct { @@ -205,6 +210,7 @@ type SpecVRFBGPNeighbor struct { BFDProfile *string `json:"bfdProfile,omitempty"` DisableConnectedCheck *bool `json:"disableConnectedCheck,omitempty"` UpdateSource *string `json:"updateSource,omitempty"` + ExtendedNexthop *bool `json:"extendedNexthop,omitempty"` } const ( @@ -672,3 +678,7 @@ func (s *SpecLSTInterface) IsNil() bool { func (s *SpecBFDProfile) IsNil() bool { return s == nil } + +func (s *SpecInterfaceIPv6) IsNil() bool { + return s == nil +} diff --git a/pkg/hhfctl/inspect/vpc.go b/pkg/hhfctl/inspect/vpc.go index ecac04422..fabd99f3e 100644 --- a/pkg/hhfctl/inspect/vpc.go +++ b/pkg/hhfctl/inspect/vpc.go @@ -61,8 +61,12 @@ func (out *VPCOut) MarshalText(_ VPCIn, now time.Time) (string, error) { str.WriteString(fmt.Sprintf(" %s:\n", subnetName)) str.WriteString(fmt.Sprintf(" Subnet: %s\n", subnetSpec.Subnet)) - str.WriteString(fmt.Sprintf(" Gateway: %s\n", subnetSpec.Gateway)) - str.WriteString(fmt.Sprintf(" VLAN: %d\n", subnetSpec.VLAN)) + if subnetSpec.HostBGP { + str.WriteString(" HostBGP: true\n") + } else { + str.WriteString(fmt.Sprintf(" Gateway: %s\n", subnetSpec.Gateway)) + str.WriteString(fmt.Sprintf(" VLAN: %d\n", subnetSpec.VLAN)) + } access, ok := out.Access[subnetName] if !ok { diff --git a/pkg/hhfctl/vpc.go b/pkg/hhfctl/vpc.go index 2b4e7687a..e01d4fee5 100644 --- a/pkg/hhfctl/vpc.go +++ b/pkg/hhfctl/vpc.go @@ -33,11 +33,12 @@ import ( ) type VPCCreateOptions struct { - Name string - Subnet string - VLAN uint16 - DHCP vpcapi.VPCDHCP - Mode vpcapi.VPCMode + Name string + Subnet string + VLAN uint16 + DHCP vpcapi.VPCDHCP + Mode vpcapi.VPCMode + HostBGP bool } func VPCCreate(ctx context.Context, printYaml bool, options *VPCCreateOptions) error { @@ -52,9 +53,10 @@ func VPCCreate(ctx context.Context, printYaml bool, options *VPCCreateOptions) e Spec: vpcapi.VPCSpec{ Subnets: map[string]*vpcapi.VPCSubnet{ "default": { - Subnet: options.Subnet, - VLAN: options.VLAN, - DHCP: options.DHCP, + Subnet: options.Subnet, + VLAN: options.VLAN, + DHCP: options.DHCP, + HostBGP: options.HostBGP, }, }, Mode: options.Mode,