Skip to content

Commit ca48b5b

Browse files
committed
cisco-nxos-provider: enable NVE
This commit enables configuring the Network Virtualization Edge (NVE) interface on cisco NXOS. It supports the following configuration: ``` interface nve1 no shutdown host-reachability protocol bgp advertise virtual-rmac source-interface loopback1 anycast loopback2 global suppress-arp global mcast-group 237.0.0.1 L2 source-interface hold-down-time 300 ``` which can be achieved with: ``` nveCfg, err := nve.NewNVE( nve.WithHostReachabilityProtocol(nve.HostReachBGP), nve.WithAdvertiseVirtualRmac(true), nve.WithSourceInterface("Loopback1"), nve.WithAnycastInterface("Loopback2"), nve.WithSuppressARP(true), nve.WithMulticastGroupL2("237.0.0.1"), nve.WithHoldDownTime(300), ) ```
1 parent bf66eb3 commit ca48b5b

File tree

2 files changed

+696
-0
lines changed

2 files changed

+696
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
package nve
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"net"
10+
"strconv"
11+
12+
"github.com/openconfig/ygot/ygot"
13+
14+
nxos "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/genyang"
15+
"github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/gnmiext"
16+
"github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/iface"
17+
)
18+
19+
var _ gnmiext.DeviceConf = (*NVE)(nil)
20+
21+
type HostReachT uint
22+
23+
const (
24+
HostReachFloodAndLearn HostReachT = iota + 1
25+
HostReachBGP
26+
)
27+
28+
var (
29+
ErrInvalidInterfaceName = errors.New("nve: invalid interface name")
30+
ErrInvalidInterfaceNumber = errors.New("nve: interface number must be between 0 and 1023")
31+
ErrInvalidHoldDownTime = errors.New("nve: hold down time must be between 1 and 1500 seconds")
32+
ErrInvalidHostReachProto = errors.New("nve: invalid host reachability protocol")
33+
ErrMissingSourceInterface = errors.New("nve: source interface must be set before setting anycast interface")
34+
ErrIdenticalInterfaceNames = errors.New("nve: source and anycast interface names must be different")
35+
)
36+
37+
// NVE represents the Network Virtualization Edge interface (nve1). This object must be
38+
// initialized with the NewNVE function.
39+
type NVE struct {
40+
adminSt bool
41+
hostReach HostReachT
42+
advertiseVirtualRmac *bool
43+
// the name of the loopback to use as source
44+
sourceInterface string
45+
// the name of the loopback to use for anycast
46+
anycastInterface string
47+
suppressARP *bool
48+
// multicast group for L2 VTEP discovery
49+
mcastL2 *net.IPAddr
50+
// multicast group for L3 VTEP discovery
51+
mcastL3 *net.IPAddr
52+
holdDownTime uint16 // in seconds
53+
}
54+
55+
type NVEOption func(*NVE) error
56+
57+
func NewNVE(opts ...NVEOption) (*NVE, error) {
58+
n := &NVE{
59+
adminSt: true,
60+
}
61+
for _, opt := range opts {
62+
if err := opt(n); err != nil {
63+
return nil, err
64+
}
65+
}
66+
return n, nil
67+
}
68+
69+
// WithAdminState sets the administrative state of the NVE interface. If not set, the default is `up`.
70+
func WithAdminState(adminSt bool) NVEOption {
71+
return func(n *NVE) error {
72+
n.adminSt = adminSt
73+
return nil
74+
}
75+
}
76+
77+
// WithHostReachabilityProtocol sets the host reachability protocol for the NVE.
78+
func WithHostReachabilityProtocol(proto HostReachT) NVEOption {
79+
return func(n *NVE) error {
80+
switch proto {
81+
case HostReachBGP, HostReachFloodAndLearn:
82+
n.hostReach = proto
83+
default:
84+
return ErrInvalidHostReachProto
85+
}
86+
return nil
87+
}
88+
}
89+
90+
// WithAdvertiseVirtualRmac enables or disables the advertisement of the virtual RMAC address for the NVE interface.
91+
func WithAdvertiseVirtualRmac(enable bool) NVEOption {
92+
return func(n *NVE) error {
93+
n.advertiseVirtualRmac = ygot.Bool(enable)
94+
return nil
95+
}
96+
}
97+
98+
// WithSourceInterface sets the source interface for the NVE. It must be a loopback interface as per the naming convention
99+
// defined in the `iface` package.
100+
func WithSourceInterface(loopback string) NVEOption {
101+
return func(n *NVE) error {
102+
loName, err := iface.ShortNameLoopback(loopback)
103+
if err != nil {
104+
return ErrInvalidInterfaceName
105+
}
106+
loNr, err := strconv.Atoi(loName[2:])
107+
if err != nil || loNr > 1023 {
108+
return ErrInvalidInterfaceNumber
109+
}
110+
n.sourceInterface = loName
111+
return nil
112+
}
113+
}
114+
115+
// WithAnycastInterface sets the anycast interface for the NVE. It must be a loopback interface as per the naming convention
116+
// defined in the `iface` package. The anycast interface must be different from the source interface. The source interface
117+
// must be set before setting the anycast interface.
118+
func WithAnycastInterface(loopback string) NVEOption {
119+
return func(n *NVE) error {
120+
if n.sourceInterface == "" {
121+
return ErrMissingSourceInterface
122+
}
123+
loName, err := iface.ShortNameLoopback(loopback)
124+
if err != nil {
125+
return ErrInvalidInterfaceName
126+
}
127+
loNr, err := strconv.Atoi(loName[2:])
128+
if err != nil || loNr > 1023 {
129+
return ErrInvalidInterfaceNumber
130+
}
131+
if loName == n.sourceInterface {
132+
return ErrIdenticalInterfaceNames
133+
}
134+
n.anycastInterface = loName
135+
return nil
136+
}
137+
}
138+
139+
// WithSuppressARP sets the NVE to suppress ARP requests for VTEP IP addresses. If not set, the NVE will
140+
// use the default behavior. When set, this is the equivalent to the configuration statement `global suppress-arp`.
141+
// This config will not be shown with `show running-config interface nve1` but only over gNMI.
142+
func WithSuppressARP(enable bool) NVEOption {
143+
return func(n *NVE) error {
144+
n.suppressARP = ygot.Bool(enable)
145+
return nil
146+
}
147+
}
148+
149+
func validateMulticastAddress(addr string) (*net.IPAddr, error) {
150+
ip := net.ParseIP(addr)
151+
if ip == nil || ip.To4() == nil || !ip.IsMulticast() {
152+
return nil, fmt.Errorf("nve: invalid multicast IPv4 address: %s", addr)
153+
}
154+
return &net.IPAddr{IP: ip}, nil
155+
}
156+
157+
// WithMulticastGroupL2 configures the global multicast group for the Layer 2 VNI.
158+
// Addr must be a valid IPv4 multicast address.
159+
func WithMulticastGroupL2(addr string) NVEOption {
160+
return func(n *NVE) error {
161+
ip, err := validateMulticastAddress(addr)
162+
if err != nil {
163+
return err
164+
}
165+
n.mcastL2 = ip
166+
return nil
167+
}
168+
}
169+
170+
// WithMulticastGroupL3 configures the global multicast group for the Layer 3 VNI.
171+
// Addr must be a valid IPv4 multicast address.
172+
func WithMulticastGroupL3(addr string) NVEOption {
173+
return func(n *NVE) error {
174+
ip, err := validateMulticastAddress(addr)
175+
if err != nil {
176+
return err
177+
}
178+
n.mcastL3 = ip
179+
return nil
180+
}
181+
}
182+
183+
// WithHoldDownTime sets the hold down time for the NVE interface in seconds (1-1500).
184+
func WithHoldDownTime(seconds uint16) NVEOption {
185+
return func(n *NVE) error {
186+
if seconds < 1 || seconds > 1500 {
187+
return ErrInvalidHoldDownTime
188+
}
189+
n.holdDownTime = seconds
190+
return nil
191+
}
192+
}
193+
194+
// ToYGOT converts the NVE configuration to these gNMI updates:
195+
// - enable the NV feature on the device
196+
// - configure the NVE interface with the provided settings
197+
// - enable the NG-MVPN feature (only if a multicast group for L3 is set)
198+
func (n *NVE) ToYGOT(_ context.Context, _ gnmiext.Client) ([]gnmiext.Update, error) {
199+
updates := []gnmiext.Update{}
200+
val := nxos.Cisco_NX_OSDevice_System_EpsItems_EpIdItems_EpList{}
201+
202+
val.AdminSt = nxos.Cisco_NX_OSDevice_Nw_AdminSt_enabled
203+
if !n.adminSt {
204+
val.AdminSt = nxos.Cisco_NX_OSDevice_Nw_AdminSt_disabled
205+
}
206+
207+
switch n.hostReach {
208+
case HostReachBGP:
209+
val.HostReach = nxos.Cisco_NX_OSDevice_Nvo_HostReachT_bgp
210+
case HostReachFloodAndLearn:
211+
val.HostReach = nxos.Cisco_NX_OSDevice_Nvo_HostReachT_Flood_and_learn
212+
default:
213+
// No-op
214+
}
215+
216+
val.AdvertiseVmac = n.advertiseVirtualRmac
217+
218+
if n.sourceInterface != "" {
219+
val.SourceInterface = ygot.String(n.sourceInterface)
220+
if n.anycastInterface != "" {
221+
val.AnycastIntf = ygot.String(n.anycastInterface)
222+
}
223+
}
224+
225+
val.SuppressARP = n.suppressARP
226+
227+
if n.mcastL2 != nil {
228+
val.McastGroupL2 = ygot.String(n.mcastL2.String())
229+
}
230+
if n.mcastL3 != nil {
231+
updates = append(updates, gnmiext.EditingUpdate{
232+
XPath: "/System/fm-items/ngmvpn-items",
233+
Value: &nxos.Cisco_NX_OSDevice_System_FmItems_NgmvpnItems{
234+
AdminSt: nxos.Cisco_NX_OSDevice_Fm_AdminState_enabled,
235+
},
236+
})
237+
val.McastGroupL3 = ygot.String(n.mcastL3.String())
238+
}
239+
240+
if n.holdDownTime != 0 {
241+
val.HoldDownTime = ygot.Uint16(n.holdDownTime)
242+
}
243+
244+
updates = append(updates, []gnmiext.Update{
245+
gnmiext.EditingUpdate{
246+
XPath: "/System/fm-items/nvo-items",
247+
Value: &nxos.Cisco_NX_OSDevice_System_FmItems_NvoItems{
248+
AdminSt: nxos.Cisco_NX_OSDevice_Fm_AdminState_enabled,
249+
},
250+
},
251+
gnmiext.ReplacingUpdate{
252+
XPath: "/System/eps-items/epId-items/Ep-list[epId=1]",
253+
Value: &val,
254+
},
255+
}...)
256+
return updates, nil
257+
}
258+
259+
// Reset removes the nve1 interface and disables the NV feature on the device.
260+
func (n *NVE) Reset(_ context.Context, _ gnmiext.Client) ([]gnmiext.Update, error) {
261+
return []gnmiext.Update{
262+
gnmiext.DeletingUpdate{
263+
XPath: "/System/eps-items/epId-items/Ep-list[epId=1]",
264+
},
265+
gnmiext.EditingUpdate{
266+
XPath: "/System/fm-items/nvo-items",
267+
Value: &nxos.Cisco_NX_OSDevice_System_FmItems_NvoItems{
268+
AdminSt: nxos.Cisco_NX_OSDevice_Fm_AdminState_disabled,
269+
},
270+
},
271+
}, nil
272+
}

0 commit comments

Comments
 (0)