Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ A provider's `EnsureX` methods must be safe to call on every reconciliation with

The `gnmiext` package already implements a Get-and-Check approach: it diffs current device state against the desired configuration and only performs a gNMI Set when a real change is needed. This makes it safe for periodic reconciliation.

**No list fields in Patched structs:**

`Patch` (gNMI update) merges into existing device config — it adds or updates list entries but never removes them. A `DataElement` passed to `Patch` must not contain `gnmiext.List` or slice fields. Instead, manage list entries as individual `DataElement`s: fetch current entries with a container struct via `GetConfig`, `Patch` each desired entry, and `Delete` stale entries not in the desired set.

**Platform default values — critical pitfall:**

Optional fields in the API spec that map to optional fields in the provider Go struct require special handling:
Expand Down
28 changes: 22 additions & 6 deletions internal/provider/cisco/nxos/bgp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ var (
_ gnmiext.DataElement = (*BGP)(nil)
_ gnmiext.DataElement = (*BGPDom)(nil)
_ gnmiext.DataElement = (*BGPDomItems)(nil)
_ gnmiext.DataElement = (*BGPDomAfItems)(nil)
_ gnmiext.DataElement = (*BGPDomAfItem)(nil)
_ gnmiext.DataElement = (*BGPPeerGroup)(nil)
)

Expand Down Expand Up @@ -45,12 +47,9 @@ func (*BGP) XPath() string {
}

type BGPDom struct {
Name string `json:"name"`
RtrID string `json:"rtrId"`
RtrIDAuto AdminSt `json:"rtrIdAuto"`
AfItems struct {
DomAfList gnmiext.List[AddressFamily, *BGPDomAfItem] `json:"DomAf-list,omitzero"`
} `json:"af-items,omitzero"`
Name string `json:"name"`
RtrID string `json:"rtrId"`
RtrIDAuto AdminSt `json:"rtrIdAuto"`
PeerContItems struct {
PeerContList gnmiext.List[string, *BGPPeerGroup] `json:"PeerCont-list,omitzero"`
} `json:"peercont-items,omitzero"`
Expand Down Expand Up @@ -87,7 +86,18 @@ func (g *BGPPeerGroup) XPath() string {
return "System/bgp-items/inst-items/dom-items/Dom-list[name=" + g.VRFName + "]/peercont-items/PeerCont-list[name=" + g.Name + "]"
}

// BGPDomAfItems is the list container for all address families under a BGP domain.
type BGPDomAfItems struct {
Name string `json:"-"` // VRF name, for XPath construction
DomAfList gnmiext.List[AddressFamily, *BGPDomAfItem] `json:"DomAf-list,omitzero"`
}

func (a *BGPDomAfItems) XPath() string {
return "System/bgp-items/inst-items/dom-items/Dom-list[name=" + a.Name + "]/af-items"
}

type BGPDomAfItem struct {
VRFName string `json:"-"` // for XPath construction
// Maximum number of equal-cost paths for iBGP
MaxEcmp int8 `json:"maxEcmp,omitempty"`
// Maximum number of equal-cost paths for eBGP
Expand Down Expand Up @@ -163,6 +173,12 @@ func (af *BGPDomAfItem) UnmarshalJSON(v []byte) error {

func (af *BGPDomAfItem) Key() AddressFamily { return af.Type }

func (*BGPDomAfItem) IsListItem() {}

func (af *BGPDomAfItem) XPath() string {
return "System/bgp-items/inst-items/dom-items/Dom-list[name=" + af.VRFName + "]/af-items/DomAf-list[type=" + string(af.Type) + "]"
}

// NewInterLeakPDirect creates an InterLeakP entry for redistributing directly
// connected routes into a BGP address family.
func NewInterLeakPDirect(rtMap string) *InterLeakP {
Expand Down
66 changes: 31 additions & 35 deletions internal/provider/cisco/nxos/bgp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,44 @@
package nxos

func init() {
bgpDom := &BGPDom{Name: DefaultVRFName, RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled}
bgpDom.AfItems.DomAfList.Set(&BGPDomAfItem{
Type: AddressFamilyL2EVPN,
RetainRttAll: AdminStEnabled,
})
Register("bgp_dom", bgpDom)
bgp := &BGP{AdminSt: AdminStEnabled, Asn: "65000"}
Register("bgp", bgp)

bgpDomVrf := &BGPDom{Name: "CC-MGMT", RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled}
Register("bgp_dom_vrf", bgpDomVrf)
Register("bgp_dom", &BGPDom{Name: DefaultVRFName, RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled})

Register("bgp_dom_vrf", &BGPDom{Name: "CC-MGMT", RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled})

Register("bgp_dom_af", &BGPDomAfItem{
VRFName: DefaultVRFName,
Type: AddressFamilyIPv4Unicast,
ExportGwIP: AdminStDisabled,
AdvertL2vpnEvpn: AdminStDisabled,
})

bgpDomAdvPip := &BGPDom{Name: DefaultVRFName, RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled}
bgpDomAdvPip.AfItems.DomAfList.Set(&BGPDomAfItem{
Register("bgp_dom_af_advpip", &BGPDomAfItem{
VRFName: DefaultVRFName,
Type: AddressFamilyL2EVPN,
AdvPip: AdminStEnabled,
RetainRttAll: AdminStEnabled,
})
Register("bgp_dom_advpip", bgpDomAdvPip)

bgp := &BGP{AdminSt: AdminStEnabled, Asn: "65000"}
Register("bgp", bgp)
Register("bgp_dom_af_exp", &BGPDomAfItem{
VRFName: DefaultVRFName,
Type: AddressFamilyIPv4Unicast,
ExportGwIP: AdminStEnabled,
AdvertL2vpnEvpn: AdminStDisabled,
})

Register("bgp_dom_af_advl2vpnevpn", &BGPDomAfItem{
VRFName: DefaultVRFName,
Type: AddressFamilyIPv4Unicast,
ExportGwIP: AdminStDisabled,
AdvertL2vpnEvpn: AdminStEnabled,
})

rdstItem := &BGPDomAfItem{VRFName: DefaultVRFName, Type: AddressFamilyIPv4Unicast, ExportGwIP: AdminStDisabled, AdvertL2vpnEvpn: AdminStDisabled}
rdstItem.InterLeakPItems.InterLeakPList.Set(NewInterLeakPDirect("ROUTE_MAP"))
Register("bgp_dom_af_rdst", rdstItem)

bgpPeer := &BGPPeer{
VRFName: DefaultVRFName,
Expand Down Expand Up @@ -62,28 +80,6 @@ func init() {
bgpPeerRp.AfItems.PeerAfList.Set(bgpPeerRpAf)
Register("bgp_dom_rp", bgpPeerRp)

bgpDomRdst := &BGPDom{Name: "CC-CLOUD01", RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled}
rdstItem := &BGPDomAfItem{Type: AddressFamilyIPv4Unicast, ExportGwIP: AdminStDisabled, AdvertL2vpnEvpn: AdminStDisabled}
rdstItem.InterLeakPItems.InterLeakPList.Set(NewInterLeakPDirect("ROUTE_MAP"))
bgpDomRdst.AfItems.DomAfList.Set(rdstItem)
Register("bgp_dom_rdst", bgpDomRdst)

bgpDomExp := &BGPDom{Name: "CC-CLOUD01", RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled}
bgpDomExp.AfItems.DomAfList.Set(&BGPDomAfItem{
Type: AddressFamilyIPv4Unicast,
ExportGwIP: AdminStEnabled,
AdvertL2vpnEvpn: AdminStDisabled,
})
Register("bgp_dom_exp", bgpDomExp)

bgpDomAdvL2vpnEvpn := &BGPDom{Name: "CC-CLOUD01", RtrID: "1.1.1.1", RtrIDAuto: AdminStDisabled}
bgpDomAdvL2vpnEvpn.AfItems.DomAfList.Set(&BGPDomAfItem{
Type: AddressFamilyIPv4Unicast,
ExportGwIP: AdminStDisabled,
AdvertL2vpnEvpn: AdminStEnabled,
})
Register("bgp_dom_advl2vpnevpn", bgpDomAdvL2vpnEvpn)

bgpPeerLocalAs := &BGPPeer{
VRFName: DefaultVRFName,
Addr: "1.1.1.1",
Expand Down
69 changes: 57 additions & 12 deletions internal/provider/cisco/nxos/intf.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ var (
_ gnmiext.DataElement = (*ICMPIf)(nil)
_ gnmiext.DataElement = (*PortChannel)(nil)
_ gnmiext.DataElement = (*PortChannelOperItems)(nil)
_ gnmiext.DataElement = (*PortChannelMemberItems)(nil)
_ gnmiext.DataElement = (*PortChannelMember)(nil)
_ gnmiext.DataElement = (*SwitchVirtualInterface)(nil)
_ gnmiext.DataElement = (*SwitchVirtualInterfaceOperItems)(nil)
_ gnmiext.DataElement = (*EncapRoutedInterface)(nil)
_ gnmiext.DataElement = (*EncapRoutedInterfaceOperItems)(nil)
_ gnmiext.DataElement = (*AddrItem)(nil)
_ gnmiext.DataElement = (*IntfAddrItems)(nil)
_ gnmiext.DataElement = (*IntfAddr)(nil)
_ gnmiext.DataElement = (*FabricFwdIf)(nil)
)

Expand Down Expand Up @@ -294,17 +298,31 @@ type PortChannel struct {
SuspIndividual AdminSt4 `json:"suspIndividual"`
UserCfgdFlags UserFlags `json:"userCfgdFlags"`
RtvrfMbrItems *VrfMember `json:"rtvrfMbr-items,omitempty"`
RsmbrIfsItems struct {
RsMbrIfsList gnmiext.List[string, *PortChannelMember] `json:"RsMbrIfs-list,omitzero"`
} `json:"rsmbrIfs-items,omitzero"`
AggrExtdItems struct {
AggrExtdItems struct {
BufferBoost AdminSt4 `json:"bufferBoost,omitempty"`
} `json:"aggrExtd-items,omitzero"`
}

func (*PortChannel) IsListItem() {}

func (p *PortChannel) XPath() string {
return "System/intf-items/aggr-items/AggrIf-list[id=" + p.ID + "]"
}

// PortChannelMemberItems is the list container for fetching port-channel members.
type PortChannelMemberItems struct {
ID string `json:"-"`
RsMbrIfsList gnmiext.List[string, *PortChannelMember] `json:"RsMbrIfs-list,omitzero"`
}

func (m *PortChannelMemberItems) XPath() string {
return "System/intf-items/aggr-items/AggrIf-list[id=" + m.ID + "]/rsmbrIfs-items"
}

type PortChannelMember struct {
TDn string `json:"tDn"`
Force bool `json:"isMbrForce,omitempty"`
PortChannelID string `json:"-"`
TDn string `json:"tDn"`
Force bool `json:"isMbrForce,omitempty"`
}

func NewPortChannelMember(name string) *PortChannelMember {
Expand All @@ -316,10 +334,13 @@ func NewPortChannelMember(name string) *PortChannelMember {

func (m *PortChannelMember) Key() string { return m.TDn }

func (*PortChannel) IsListItem() {}
func (*PortChannelMember) IsListItem() {}

func (p *PortChannel) XPath() string {
return "System/intf-items/aggr-items/AggrIf-list[id=" + p.ID + "]"
func (m *PortChannelMember) XPath() string {
// Escape brackets in tDn so ygot.StringToStructuredPath does not
// treat them as key delimiters.
tDn := strings.NewReplacer("[", `\[`, "]", `\]`).Replace(m.TDn)
return "System/intf-items/aggr-items/AggrIf-list[id=" + m.PortChannelID + "]/rsmbrIfs-items/RsMbrIfs-list[tDn=" + tDn + "]"
}

type PortChannelOperItems struct {
Expand Down Expand Up @@ -406,9 +427,6 @@ func (d *AddrDom) Key() string { return d.Name }
type AddrItem struct {
ID string `json:"id"`
Unnumbered string `json:"unnumbered,omitempty"`
AddrItems struct {
AddrList gnmiext.List[string, *IntfAddr] `json:"Addr-list,omitzero"`
} `json:"addr-items,omitzero"`

// Is6 indicates whether the addresses are IPv6 (true) or IPv4 (false).
// This field is not serialized to JSON and is only used internally to
Expand All @@ -432,7 +450,25 @@ func (a *AddrItem) XPath() string {
return "System/ipv4-items/inst-items/dom-items/Dom-list[name=" + a.Vrf + "]/if-items/If-list[id=" + a.ID + "]"
}

// IntfAddrItems is the list container for fetching addresses on an interface.
type IntfAddrItems struct {
ID string `json:"-"`
Vrf string `json:"-"`
Is6 bool `json:"-"`
AddrList gnmiext.List[string, *IntfAddr] `json:"Addr-list,omitzero"`
}

func (a *IntfAddrItems) XPath() string {
if a.Is6 {
return "System/ipv6-items/inst-items/dom-items/Dom-list[name=" + a.Vrf + "]/if-items/If-list[id=" + a.ID + "]/addr-items"
}
return "System/ipv4-items/inst-items/dom-items/Dom-list[name=" + a.Vrf + "]/if-items/If-list[id=" + a.ID + "]/addr-items"
}

type IntfAddr struct {
ID string `json:"-"`
Vrf string `json:"-"`
Is6 bool `json:"-"`
Addr string `json:"addr"`
Pref int `json:"pref"`
Tag int `json:"tag"`
Expand All @@ -441,6 +477,15 @@ type IntfAddr struct {

func (a *IntfAddr) Key() string { return a.Addr }

func (*IntfAddr) IsListItem() {}

func (a *IntfAddr) XPath() string {
if a.Is6 {
return "System/ipv6-items/inst-items/dom-items/Dom-list[name=" + a.Vrf + "]/if-items/If-list[id=" + a.ID + "]/addr-items/Addr-list[addr=" + a.Addr + "]"
}
return "System/ipv4-items/inst-items/dom-items/Dom-list[name=" + a.Vrf + "]/if-items/If-list[id=" + a.ID + "]/addr-items/Addr-list[addr=" + a.Addr + "]"
}

type IntfAddrType string

const (
Expand Down
24 changes: 13 additions & 11 deletions internal/provider/cisco/nxos/intf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,17 @@ func init() {
Descr: NewOption("L3 Subinterface on eth1/1"),
})

intfAddr4 := &AddrItem{ID: "lo0", Vrf: DefaultVRFName}
intfAddr4.AddrItems.AddrList.Set(&IntfAddr{
Register("intf_addr4", &IntfAddr{
ID: "lo0",
Vrf: DefaultVRFName,
Is6: false,
Addr: "10.0.0.10/32",
Pref: 0,
Tag: 0,
Type: "primary",
})
Register("intf_addr4", intfAddr4)

pc := &PortChannel{
Register("pc", &PortChannel{
AccessVlan: DefaultVLAN,
AdminSt: AdminStUp,
Descr: NewOption("vPC Leaf1 to Host1"),
Expand All @@ -151,9 +152,12 @@ func init() {
NativeVlan: DefaultVLAN,
SuspIndividual: AdminStEnable,
UserCfgdFlags: UserFlagAdminState,
}
pc.RsmbrIfsItems.RsMbrIfsList.Set(NewPortChannelMember("eth1/10"))
Register("pc", pc)
})

pcMember := NewPortChannelMember("eth1/10")
pcMember.PortChannelID = "po10"
Register("pc_member", pcMember)

Register("pc_trunk_vlans", &TrunkVlans{IfName: "po10", Vlans: "10"})

Register("pc_rtd", &PortChannel{
Expand All @@ -176,7 +180,7 @@ func init() {
}{BufferBoost: AdminStEnable},
})

pcLacp := &PortChannel{
Register("pc_lacp", &PortChannel{
AccessVlan: DefaultVLAN,
AdminSt: AdminStUp,
Descr: NewOption("vPC Leaf1 to Host1 (LACP)"),
Expand All @@ -190,9 +194,7 @@ func init() {
NativeVlan: DefaultVLAN,
SuspIndividual: AdminStDisable,
UserCfgdFlags: UserFlagAdminState,
}
pcLacp.RsmbrIfsItems.RsMbrIfsList.Set(NewPortChannelMember("eth1/1"))
Register("pc_lacp", pcLacp)
})

svi := &SwitchVirtualInterface{
AdminSt: AdminStUp,
Expand Down
27 changes: 21 additions & 6 deletions internal/provider/cisco/nxos/lldp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,33 @@ package nxos

import "github.com/ironcore-dev/network-operator/internal/transport/gnmiext"

var _ gnmiext.DataElement = (*LLDP)(nil)
var (
_ gnmiext.DataElement = (*LLDP)(nil)
_ gnmiext.DataElement = (*LLDPIfItems)(nil)
_ gnmiext.DataElement = (*LLDPIfItem)(nil)
)

type LLDP struct {
// HoldTime is the number of seconds that a receiving device should hold the information sent by another device before discarding it.
HoldTime Option[uint16] `json:"holdTime"`
// InitDelay is the number of seconds for LLDP to initialize on any interface.
InitDelay Option[uint16] `json:"initDelayTime"`
// IfItems contains the per-interface LLDP configuration.
IfItems struct {
IfList gnmiext.List[string, *LLDPIfItem] `json:"If-list,omitzero"`
} `json:"if-items,omitzero"`
}

func (*LLDP) IsListItem() {}

func (*LLDP) XPath() string {
return "System/lldp-items/inst-items"
}

func (*LLDP) IsListItem() {}
// LLDPIfItems is the list container for fetching per-interface LLDP configuration.
type LLDPIfItems struct {
IfList gnmiext.List[string, *LLDPIfItem] `json:"If-list,omitzero"`
}

func (*LLDPIfItems) XPath() string {
return "System/lldp-items/inst-items/if-items"
}

type LLDPIfItem struct {
InterfaceName string `json:"id"`
Expand All @@ -32,6 +41,12 @@ type LLDPIfItem struct {

func (i *LLDPIfItem) Key() string { return i.InterfaceName }

func (*LLDPIfItem) IsListItem() {}

func (i *LLDPIfItem) XPath() string {
return "System/lldp-items/inst-items/if-items/If-list[id=" + i.InterfaceName + "]"
}

type LLDPOper struct {
OperSt OperSt `json:"operSt"`
}
Expand Down
Loading
Loading