From ca43c611caba61c8df17fed156f22c0c0a0c689a Mon Sep 17 00:00:00 2001 From: thineshvs Date: Mon, 27 Jul 2026 13:24:54 +0000 Subject: [PATCH 1/7] Add SAI L2 bridging, FDB learning, and BUM flooding support --- dataplane/forwarding/fwdconfig/action.go | 22 +++++++ dataplane/saiserver/fdb.go | 73 ++++++++++++++++++++++++ dataplane/saiserver/ports.go | 14 +++-- dataplane/saiserver/routing.go | 57 +++++++++++++++++- dataplane/saiserver/switch.go | 35 ++++++++++++ 5 files changed, 196 insertions(+), 5 deletions(-) diff --git a/dataplane/forwarding/fwdconfig/action.go b/dataplane/forwarding/fwdconfig/action.go index a64e2e21..803af4fd 100644 --- a/dataplane/forwarding/fwdconfig/action.go +++ b/dataplane/forwarding/fwdconfig/action.go @@ -409,3 +409,25 @@ func (m *MirrorActionBuilder) set(a *fwdpb.ActionDesc) { func (m *MirrorActionBuilder) actionType() fwdpb.ActionType { return fwdpb.ActionType_ACTION_TYPE_MIRROR } + +// BridgeLearnActionBuilder is a builder for a bridge learn action. +type BridgeLearnActionBuilder struct { + tableID string +} + +// BridgeLearnAction returns a new bridge learn action builder. +func BridgeLearnAction(tableID string) *BridgeLearnActionBuilder { + return &BridgeLearnActionBuilder{tableID: tableID} +} + +func (b *BridgeLearnActionBuilder) set(a *fwdpb.ActionDesc) { + a.Action = &fwdpb.ActionDesc_Bridge{ + Bridge: &fwdpb.BridgeLearnActionDesc{ + TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: b.tableID}}, + }, + } +} + +func (b *BridgeLearnActionBuilder) actionType() fwdpb.ActionType { + return fwdpb.ActionType_ACTION_TYPE_BRIDGE_LEARN +} diff --git a/dataplane/saiserver/fdb.go b/dataplane/saiserver/fdb.go index 2f1350ec..f21174aa 100644 --- a/dataplane/saiserver/fdb.go +++ b/dataplane/saiserver/fdb.go @@ -16,11 +16,16 @@ package saiserver import ( "context" + "fmt" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/openconfig/lemming/dataplane/forwarding/fwdconfig" saipb "github.com/openconfig/lemming/dataplane/proto/sai" "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" + fwdpb "github.com/openconfig/lemming/proto/forwarding" ) type fdb struct { @@ -41,3 +46,71 @@ func newFdb(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server) func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesRequest) (*saipb.FlushFdbEntriesResponse, error) { return &saipb.FlushFdbEntriesResponse{}, nil } + +func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryRequest) (*saipb.CreateFdbEntryResponse, error) { + entry := req.GetEntry() + if entry == nil { + return nil, status.Errorf(codes.InvalidArgument, "FDB entry is required") + } + + mac := entry.GetMacAddress() + if len(mac) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "MAC address is required") + } + + portOID := req.GetBridgePortId() + if portOID == 0 { + return nil, status.Errorf(codes.InvalidArgument, "Bridge port ID is required") + } + + bpReq := &saipb.GetBridgePortAttributeRequest{ + Oid: portOID, + AttrType: []saipb.BridgePortAttr{saipb.BridgePortAttr_BRIDGE_PORT_ATTR_PORT_ID}, + } + bpResp := &saipb.GetBridgePortAttributeResponse{} + if err := f.mgr.PopulateAttributes(bpReq, bpResp); err != nil { + return nil, fmt.Errorf("failed to populate bridge port %d: %v", portOID, err) + } + + portID := bpResp.GetAttr().GetPortId() + if portID == 0 { + return nil, fmt.Errorf("cannot find port ID for bridge port %d", portOID) + } + + addReq := fwdconfig.TableEntryAddRequest(f.dataplane.ID(), FDBTable).AppendEntry( + fwdconfig.EntryDesc(fwdconfig.ExactEntry( + fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST).WithBytes(mac), + )), + fwdconfig.TransmitAction(fmt.Sprint(portID)), + ).Build() + + if _, err := f.dataplane.TableEntryAdd(ctx, addReq); err != nil { + return nil, fmt.Errorf("failed to add FDB entry to dataplane: %v", err) + } + + return &saipb.CreateFdbEntryResponse{}, nil +} + +func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryRequest) (*saipb.RemoveFdbEntryResponse, error) { + entry := req.GetEntry() + if entry == nil { + return nil, status.Errorf(codes.InvalidArgument, "FDB entry is required") + } + + mac := entry.GetMacAddress() + if len(mac) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "MAC address is required") + } + + delReq := fwdconfig.TableEntryRemoveRequest(f.dataplane.ID(), FDBTable).AppendEntry( + fwdconfig.EntryDesc(fwdconfig.ExactEntry( + fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST).WithBytes(mac), + )), + ).Build() + + if _, err := f.dataplane.TableEntryRemove(ctx, delReq); err != nil { + return nil, fmt.Errorf("failed to remove FDB entry from dataplane: %v", err) + } + + return &saipb.RemoveFdbEntryResponse{}, nil +} diff --git a/dataplane/saiserver/ports.go b/dataplane/saiserver/ports.go index 9ab6f834..d94901f3 100644 --- a/dataplane/saiserver/ports.go +++ b/dataplane/saiserver/ports.go @@ -16,6 +16,7 @@ package saiserver import ( "context" + "encoding/binary" "fmt" "log/slog" "net" @@ -64,6 +65,7 @@ var getInterface = net.InterfaceByName func getPreIngressPipeline() []*fwdpb.ActionDesc { return []*fwdpb.ActionDesc{ fwdconfig.Action(fwdconfig.LookupAction(tunTermTable)).Build(), // Decap the packet if we have a tunnel. + fwdconfig.Action(fwdconfig.LookupAction(VlanTable)).Build(), // Classify incoming packets to VLAN. fwdconfig.Action(fwdconfig.LookupAction(inputIfaceTable)).Build(), // Match packet to interface. fwdconfig.Action(fwdconfig.LookupAction(IngressVRFTable)).Build(), // Match interface to VRF. fwdconfig.Action(fwdconfig.LookupAction(PreIngressActionTable)).Build(), // Run pre-ingress actions. @@ -101,7 +103,9 @@ func getL3Pipeline(skipIPValidation bool) []*fwdpb.ActionDesc { func getL2Pipeline() []*fwdpb.ActionDesc { return []*fwdpb.ActionDesc{ fwdconfig.Action(fwdconfig.LookupAction(IngressActionTable)).Build(), // Run ingress action. - fwdconfig.Action(fwdconfig.LookupAction(outputTable)).Build(), // Take final decision on forward, drop, or trap. + fwdconfig.Action(fwdconfig.BridgeLearnAction(FDBTable)).Build(), // Learn source MAC into FDB. + fwdconfig.Action(fwdconfig.LookupAction(FDBTable)).Build(), // Match dest MAC in FDB. + fwdconfig.Action(fwdconfig.LookupAction(FloodTable)).Build(), // Match VLAN tag if FDB misses. { ActionType: fwdpb.ActionType_ACTION_TYPE_OUTPUT, }, @@ -301,8 +305,10 @@ func (port *port) CreatePort(ctx context.Context, req *saipb.CreatePortRequest) Update: &fwdpb.PortUpdateDesc{ Port: &fwdpb.PortUpdateDesc_Kernel{ Kernel: &fwdpb.KernelPortUpdateDesc{ - Inputs: getPreIngressPipeline(), - Outputs: nil, + Inputs: getPreIngressPipeline(), + Outputs: []*fwdpb.ActionDesc{ + fwdconfig.Action(fwdconfig.DecapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET_VLAN)).Build(), + }, }, }, }, @@ -361,7 +367,7 @@ func (port *port) CreatePort(ctx context.Context, req *saipb.CreatePortRequest) fwdconfig.EntryDesc(fwdconfig.ExactEntry(fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_PORT_INPUT).WithUint64(nid.GetNid())))).Build() vlanReq.Entries[0].Actions = []*fwdpb.ActionDesc{ fwdconfig.Action(fwdconfig.EncapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET_VLAN)).Build(), - fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_SET, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_VLAN_TAG).WithUint64Value(uint64(attrs.GetPortVlanId()))).Build(), + fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_SET, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_VLAN_TAG).WithValue(binary.BigEndian.AppendUint16(nil, uint16(attrs.GetPortVlanId())))).Build(), } if _, err := port.dataplane.TableEntryAdd(ctx, vlanReq); err != nil { return nil, err diff --git a/dataplane/saiserver/routing.go b/dataplane/saiserver/routing.go index 2ad1f061..4c75b22b 100644 --- a/dataplane/saiserver/routing.go +++ b/dataplane/saiserver/routing.go @@ -1290,6 +1290,36 @@ func (vlan *vlan) CreateVlan(ctx context.Context, r *saipb.CreateVlanRequest) (* vlan.mgr.StoreAttributes(id, attrs) vlan.vlans[id] = map[uint64]*vlanMember{} vlan.oidByVId[r.GetVlanId()] = id + + floodID := fmt.Sprintf("vlan_flood_%d", r.GetVlanId()) + _, _ = vlan.dataplane.PortCreate(ctx, &fwdpb.PortCreateRequest{ + ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, + Port: &fwdpb.PortDesc{ + PortType: fwdpb.PortType_PORT_TYPE_AGGREGATE_PORT, + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: floodID}}, + }, + }) + _, _ = vlan.dataplane.PortUpdate(ctx, &fwdpb.PortUpdateRequest{ + ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: floodID}}, + Update: &fwdpb.PortUpdateDesc{ + Port: &fwdpb.PortUpdateDesc_AggregateAlgo{ + AggregateAlgo: &fwdpb.AggregatePortAlgorithmUpdateDesc{ + Hash: fwdpb.AggregateHashAlgorithm_AGGREGATE_HASH_ALGORITHM_FLOOD, + }, + }, + }, + }) + floodReq := fwdconfig.TableEntryAddRequest(vlan.dataplane.ID(), FloodTable).AppendEntry( + fwdconfig.EntryDesc(fwdconfig.ExactEntry( + fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_VLAN_TAG).WithBytes(binary.BigEndian.AppendUint16(nil, uint16(r.GetVlanId()))), + )), + fwdconfig.TransmitAction(floodID), + ).Build() + if _, err := vlan.dataplane.TableEntryAdd(ctx, floodReq); err != nil { + return nil, err + } + return &saipb.CreateVlanResponse{ Oid: id, }, nil @@ -1371,11 +1401,24 @@ func (vlan *vlan) CreateVlanMember(ctx context.Context, r *saipb.CreateVlanMembe fwdconfig.EntryDesc(fwdconfig.ExactEntry(fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_PORT_INPUT).WithUint64(nid.GetNid())))).Build() vlanReq.Entries[0].Actions = []*fwdpb.ActionDesc{ fwdconfig.Action(fwdconfig.EncapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET_VLAN)).Build(), - fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_SET, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_VLAN_TAG).WithUint64Value(uint64(vId))).Build(), + fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_SET, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_VLAN_TAG).WithValue(binary.BigEndian.AppendUint16(nil, uint16(vId)))).Build(), } if _, err := vlan.dataplane.TableEntryAdd(ctx, vlanReq); err != nil { return nil, err } + floodID := fmt.Sprintf("vlan_flood_%d", vId) + _, _ = vlan.dataplane.PortUpdate(ctx, &fwdpb.PortUpdateRequest{ + ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: floodID}}, + Update: &fwdpb.PortUpdateDesc{ + Port: &fwdpb.PortUpdateDesc_AggregateAdd{ + AggregateAdd: &fwdpb.AggregatePortAddMemberUpdateDesc{ + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: fmt.Sprint(portID)}}, + InstanceCount: 1, + }, + }, + }, + }) // Update the attributes and intenal data. vlanAttrReq := &saipb.GetVlanAttributeRequest{Oid: vOid, AttrType: []saipb.VlanAttr{saipb.VlanAttr_VLAN_ATTR_MEMBER_LIST}} vlanAttrResp := &saipb.GetVlanAttributeResponse{} @@ -1444,6 +1487,18 @@ func (vlan *vlan) RemoveVlanMember(ctx context.Context, r *saipb.RemoveVlanMembe fwdconfig.EntryDesc(fwdconfig.ExactEntry(fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_PORT_INPUT).WithUint64(nid.GetNid())))).Build()); err != nil { return nil, err } + floodID := fmt.Sprintf("vlan_flood_%d", member.Vid) + _, _ = vlan.dataplane.PortUpdate(ctx, &fwdpb.PortUpdateRequest{ + ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: floodID}}, + Update: &fwdpb.PortUpdateDesc{ + Port: &fwdpb.PortUpdateDesc_AggregateDel{ + AggregateDel: &fwdpb.AggregatePortRemoveMemberUpdateDesc{ + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: fmt.Sprint(member.PortID)}}, + }, + }, + }, + }) delete(vlan.vlans[targetVlanOid], r.GetOid()) diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index 7ada9f3d..e77d926f 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -191,6 +191,8 @@ const ( tunTermTable = "tun-term" VlanTable = "vlan" L2MCGroupTable = "l2mcg" + FDBTable = "fdb" + FloodTable = "flood" policerTabler = "policerTable" invalidIngress = "invalid-ingress" invalidIngressV4Table = "invalid-ingress-v4" @@ -445,6 +447,39 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ if _, err := sw.dataplane.TableCreate(ctx, l2mcGroupReq); err != nil { return nil, err } + fdbReq := &fwdpb.TableCreateRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Desc: &fwdpb.TableDesc{ + TableType: fwdpb.TableType_TABLE_TYPE_BRIDGE, + TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: FDBTable}}, + Table: &fwdpb.TableDesc_Bridge{ + Bridge: &fwdpb.BridgeTableDesc{}, + }, + }, + } + if _, err := sw.dataplane.TableCreate(ctx, fdbReq); err != nil { + return nil, err + } + floodReq := &fwdpb.TableCreateRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Desc: &fwdpb.TableDesc{ + TableType: fwdpb.TableType_TABLE_TYPE_EXACT, + TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: FloodTable}}, + Actions: []*fwdpb.ActionDesc{{ActionType: fwdpb.ActionType_ACTION_TYPE_DROP}}, + Table: &fwdpb.TableDesc_Exact{ + Exact: &fwdpb.ExactTableDesc{ + FieldIds: []*fwdpb.PacketFieldId{{ + Field: &fwdpb.PacketField{ + FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_VLAN_TAG, + }, + }}, + }, + }, + }, + } + if _, err := sw.dataplane.TableCreate(ctx, floodReq); err != nil { + return nil, err + } action := &fwdpb.TableCreateRequest{ ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, Desc: &fwdpb.TableDesc{ From c06fd33c2044136bde2e91c61547b6a9a0c6ba07 Mon Sep 17 00:00:00 2001 From: Abhishek Verma Date: Wed, 5 Aug 2026 10:01:58 +0000 Subject: [PATCH 2/7] add notification to syncd for fdb updates --- dataplane/saiserver/fdb.go | 68 ++++++++++++++++++++++++++++++-- dataplane/saiserver/saiserver.go | 1 + dataplane/saiserver/switch.go | 26 ++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/dataplane/saiserver/fdb.go b/dataplane/saiserver/fdb.go index f21174aa..78a2e0dc 100644 --- a/dataplane/saiserver/fdb.go +++ b/dataplane/saiserver/fdb.go @@ -17,10 +17,13 @@ package saiserver import ( "context" "fmt" + "log/slog" + "sync" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/openconfig/lemming/dataplane/forwarding/fwdconfig" saipb "github.com/openconfig/lemming/dataplane/proto/sai" @@ -30,19 +33,48 @@ import ( type fdb struct { saipb.UnimplementedFdbServer - mgr *attrmgr.AttrMgr - dataplane switchDataplaneAPI + mgr *attrmgr.AttrMgr + dataplane switchDataplaneAPI + mu sync.RWMutex + subscribers map[chan *saipb.FdbEventNotificationResponse]struct{} } func newFdb(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server) (*fdb, error) { f := &fdb{ - mgr: mgr, - dataplane: dataplane, + mgr: mgr, + dataplane: dataplane, + subscribers: make(map[chan *saipb.FdbEventNotificationResponse]struct{}), } saipb.RegisterFdbServer(s, f) return f, nil } +func (f *fdb) subscribe(ch chan *saipb.FdbEventNotificationResponse) func() { + f.mu.Lock() + defer f.mu.Unlock() + f.subscribers[ch] = struct{}{} + return func() { + f.mu.Lock() + defer f.mu.Unlock() + delete(f.subscribers, ch) + } +} + +func (f *fdb) sendNotification(data *saipb.FdbEventNotificationData) { + f.mu.RLock() + defer f.mu.RUnlock() + resp := &saipb.FdbEventNotificationResponse{ + Data: []*saipb.FdbEventNotificationData{data}, + } + for ch := range f.subscribers { + select { + case ch <- resp: + default: + slog.Warn("fdb notification channel full, dropping event") + } + } +} + func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesRequest) (*saipb.FlushFdbEntriesResponse, error) { return &saipb.FlushFdbEntriesResponse{}, nil } @@ -88,6 +120,25 @@ func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryReque return nil, fmt.Errorf("failed to add FDB entry to dataplane: %v", err) } + f.sendNotification(&saipb.FdbEventNotificationData{ + EventType: saipb.FdbEvent_FDB_EVENT_LEARNED, + FdbEntry: &saipb.FdbEntry{ + SwitchId: entry.GetSwitchId(), + MacAddress: mac, + BvId: entry.GetBvId(), + }, + Attrs: []*saipb.FdbEntryAttribute{ + { + AttributeId: proto.Int32(int32(saipb.FdbEntryAttr_FDB_ENTRY_ATTR_BRIDGE_PORT_ID)), + Value: &saipb.AttributeValue{ + Value: &saipb.AttributeValue_Oid{ + Oid: portOID, + }, + }, + }, + }, + }) + return &saipb.CreateFdbEntryResponse{}, nil } @@ -112,5 +163,14 @@ func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryReque return nil, fmt.Errorf("failed to remove FDB entry from dataplane: %v", err) } + f.sendNotification(&saipb.FdbEventNotificationData{ + EventType: saipb.FdbEvent_FDB_EVENT_AGED, + FdbEntry: &saipb.FdbEntry{ + SwitchId: entry.GetSwitchId(), + MacAddress: mac, + BvId: entry.GetBvId(), + }, + }) + return &saipb.RemoveFdbEntryResponse{}, nil } diff --git a/dataplane/saiserver/saiserver.go b/dataplane/saiserver/saiserver.go index 15c8bc59..66307d87 100644 --- a/dataplane/saiserver/saiserver.go +++ b/dataplane/saiserver/saiserver.go @@ -200,6 +200,7 @@ func New(ctx context.Context, mgr *attrmgr.AttrMgr, s *grpc.Server, opts *dplane if err != nil { return nil, err } + sw.fdb = fdb srv := &Server{ mgr: mgr, diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index e77d926f..2eb844ad 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -24,6 +24,8 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "github.com/openconfig/lemming/dataplane/dplaneopts" @@ -45,6 +47,7 @@ type saiSwitch struct { vlan *vlan stp *stp bridge *bridge + fdb *fdb hostif *hostif hash *hash isolationGroup *isolationGroup @@ -1264,6 +1267,29 @@ func (sw *saiSwitch) PortStateChangeNotification(_ *saipb.PortStateChangeNotific } } +func (sw *saiSwitch) FdbEventNotification(_ *saipb.FdbEventNotificationRequest, srv grpc.ServerStreamingServer[saipb.FdbEventNotificationResponse]) error { + if sw.fdb == nil { + return status.Error(codes.Unimplemented, "FDB notification service unavailable") + } + ch := make(chan *saipb.FdbEventNotificationResponse, 100) + unsubscribe := sw.fdb.subscribe(ch) + defer unsubscribe() + + for { + select { + case <-srv.Context().Done(): + return srv.Context().Err() + case resp, ok := <-ch: + if !ok { + return nil + } + if err := srv.Send(resp); err != nil { + return err + } + } + } +} + func (sw *saiSwitch) Reset() { sw.vlan.Reset() sw.port.Reset() From 0a282c1af0ddea2d3633c6df257075816dd1be15 Mon Sep 17 00:00:00 2001 From: Abhishek Verma Date: Wed, 5 Aug 2026 11:58:18 +0000 Subject: [PATCH 3/7] Add dynamic bridge MAC learning notification handler --- .../forwarding/fwdtable/bridge/bridge.go | 16 +++++++-- dataplane/saiserver/switch.go | 35 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/dataplane/forwarding/fwdtable/bridge/bridge.go b/dataplane/forwarding/fwdtable/bridge/bridge.go index 7d115ffa..ca06420b 100644 --- a/dataplane/forwarding/fwdtable/bridge/bridge.go +++ b/dataplane/forwarding/fwdtable/bridge/bridge.go @@ -69,9 +69,17 @@ func (req *learnRequest) DebugString(port fwdport.Port) string { // is buffered. type Table struct { *exact.Table // exact table containing mac entries - learn *queue.Queue // unbounded queue for learn requests - ctx *fwdcontext.Context // context for finding objects - notify chan bool // if not nil, a notification is generated when an entry is learned (test only) + learn *queue.Queue // unbounded queue for learn requests + ctx *fwdcontext.Context // context for finding objects + notify chan bool // if not nil, a notification is generated when an entry is learned (test only) + LearnCallback func(mac []byte, portID string) +} + +// SetLearnCallback sets a callback invoked when a new MAC entry is dynamically learned. +func (t *Table) SetLearnCallback(cb func(mac []byte, portID string)) { + t.ctx.Lock() + defer t.ctx.Unlock() + t.LearnCallback = cb } // Clear clears the table by deleting all its entries. @@ -152,6 +160,8 @@ func (t *Table) processLearn(v interface{}) { // we try to learn a mac address that has a static entry. if err := t.AddEntry(desc, []*fwdpb.ActionDesc{&ad}); err != nil { log.Infof("bridge: Skipping learn for %v %v.", req.DebugString(port), err) + } else if t.LearnCallback != nil { + t.LearnCallback(req.mac, string(fwdport.GetID(port).GetObjectId().GetId())) } } diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index 2eb844ad..a277952b 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -30,6 +30,7 @@ import ( "github.com/openconfig/lemming/dataplane/dplaneopts" "github.com/openconfig/lemming/dataplane/forwarding/fwdconfig" + "github.com/openconfig/lemming/dataplane/forwarding/fwdtable/bridge" "github.com/openconfig/lemming/dataplane/forwarding/infra/fwdcontext" "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" @@ -463,6 +464,13 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ if _, err := sw.dataplane.TableCreate(ctx, fdbReq); err != nil { return nil, err } + if fwdCtx, err := sw.dataplane.FindContext(&fwdpb.ContextId{Id: sw.dataplane.ID()}); err == nil { + if obj, err := fwdCtx.Objects.FindID(&fwdpb.ObjectId{Id: FDBTable}); err == nil { + if brTable, ok := obj.(*bridge.Table); ok { + brTable.SetLearnCallback(sw.onBridgeLearn) + } + } + } floodReq := &fwdpb.TableCreateRequest{ ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, Desc: &fwdpb.TableDesc{ @@ -1290,6 +1298,33 @@ func (sw *saiSwitch) FdbEventNotification(_ *saipb.FdbEventNotificationRequest, } } +func (sw *saiSwitch) onBridgeLearn(mac []byte, portID string) { + if sw.fdb == nil { + return + } + portNum, err := strconv.ParseUint(portID, 10, 64) + if err != nil { + slog.Warn("onBridgeLearn: failed to parse numeric port ID", "portID", portID, "err", err) + return + } + sw.fdb.sendNotification(&saipb.FdbEventNotificationData{ + EventType: saipb.FdbEvent_FDB_EVENT_LEARNED, + FdbEntry: &saipb.FdbEntry{ + MacAddress: mac, + }, + Attrs: []*saipb.FdbEntryAttribute{ + { + AttributeId: proto.Int32(int32(saipb.FdbEntryAttr_FDB_ENTRY_ATTR_BRIDGE_PORT_ID)), + Value: &saipb.AttributeValue{ + Value: &saipb.AttributeValue_Oid{ + Oid: portNum, + }, + }, + }, + }, + }) +} + func (sw *saiSwitch) Reset() { sw.vlan.Reset() sw.port.Reset() From 8a29b02bcd6969b7ca318ad8969397e6cbf2b2c8 Mon Sep 17 00:00:00 2001 From: thineshvs Date: Wed, 19 Aug 2026 06:15:30 +0000 Subject: [PATCH 4/7] l2 corrected - show vlan brief --- dataplane/saiserver/BUILD | 1 + dataplane/saiserver/attrmgr/attrmgr.go | 16 ++++ dataplane/saiserver/fdb.go | 9 +-- dataplane/saiserver/routing.go | 9 +++ dataplane/saiserver/switch.go | 101 ++++++++++++++++++++++--- dataplane/standalone/sai/common.cc | 97 +++++++++++++++++++++++- dataplane/standalone/sai/common.h | 82 ++++++++++++++++++++ dataplane/standalone/sai/switch.cc | 21 ++++- 8 files changed, 317 insertions(+), 19 deletions(-) diff --git a/dataplane/saiserver/BUILD b/dataplane/saiserver/BUILD index 4bae291e..a7837e76 100644 --- a/dataplane/saiserver/BUILD +++ b/dataplane/saiserver/BUILD @@ -23,6 +23,7 @@ go_library( deps = [ "//dataplane/dplaneopts", "//dataplane/forwarding", + "//dataplane/forwarding/fwdtable/bridge", "//dataplane/forwarding/fwdconfig", "//dataplane/forwarding/infra/fwdcontext", "//dataplane/proto/packetio", diff --git a/dataplane/saiserver/attrmgr/attrmgr.go b/dataplane/saiserver/attrmgr/attrmgr.go index 976ed38f..70e16074 100644 --- a/dataplane/saiserver/attrmgr/attrmgr.go +++ b/dataplane/saiserver/attrmgr/attrmgr.go @@ -18,6 +18,7 @@ package attrmgr import ( + "strconv" "context" "fmt" "log/slog" @@ -385,3 +386,18 @@ func (mgr *AttrMgr) getID(req, resp proto.Message) (string, error) { } return string(pBytes), nil } + +// GetOIDsByType returns a list of OIDs that match the given ObjectType. +func (mgr *AttrMgr) GetOIDsByType(t saipb.ObjectType) []uint64 { + mgr.mu.Lock() + defer mgr.mu.Unlock() + var oids []uint64 + for id, ty := range mgr.idToType { + if ty == t { + if oid, err := strconv.ParseUint(id, 10, 64); err == nil { + oids = append(oids, oid) + } + } + } + return oids +} diff --git a/dataplane/saiserver/fdb.go b/dataplane/saiserver/fdb.go index 78a2e0dc..256dfffc 100644 --- a/dataplane/saiserver/fdb.go +++ b/dataplane/saiserver/fdb.go @@ -80,6 +80,7 @@ func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesReq } func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryRequest) (*saipb.CreateFdbEntryResponse, error) { + slog.InfoContext(ctx, "CreateFdbEntry called", "mac", req.GetEntry().GetMacAddress(), "vlan", req.GetEntry().GetBvId(), "bridge_port", req.GetBridgePortId()) entry := req.GetEntry() if entry == nil { return nil, status.Errorf(codes.InvalidArgument, "FDB entry is required") @@ -129,12 +130,7 @@ func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryReque }, Attrs: []*saipb.FdbEntryAttribute{ { - AttributeId: proto.Int32(int32(saipb.FdbEntryAttr_FDB_ENTRY_ATTR_BRIDGE_PORT_ID)), - Value: &saipb.AttributeValue{ - Value: &saipb.AttributeValue_Oid{ - Oid: portOID, - }, - }, + BridgePortId: proto.Uint64(portOID), }, }, }) @@ -143,6 +139,7 @@ func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryReque } func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryRequest) (*saipb.RemoveFdbEntryResponse, error) { + slog.InfoContext(ctx, "RemoveFdbEntry called", "mac", req.GetEntry().GetMacAddress(), "vlan", req.GetEntry().GetBvId()) entry := req.GetEntry() if entry == nil { return nil, status.Errorf(codes.InvalidArgument, "FDB entry is required") diff --git a/dataplane/saiserver/routing.go b/dataplane/saiserver/routing.go index 4c75b22b..b5ed8b9b 100644 --- a/dataplane/saiserver/routing.go +++ b/dataplane/saiserver/routing.go @@ -1250,6 +1250,8 @@ func (vlan *vlan) memberByOid(oid uint64) *vlanMember { } func (vlan *vlan) memberByPortId(oid uint64) *vlanMember { + vlan.mu.Lock() + defer vlan.mu.Unlock() for _, v := range vlan.vlans { for _, member := range v { if member.PortID == oid { @@ -1260,6 +1262,13 @@ func (vlan *vlan) memberByPortId(oid uint64) *vlanMember { return nil } +func (vlan *vlan) oidByVid(vid uint32) (uint64, bool) { + vlan.mu.Lock() + defer vlan.mu.Unlock() + oid, ok := vlan.oidByVId[vid] + return oid, ok +} + func (vlan *vlan) CreateVlan(ctx context.Context, r *saipb.CreateVlanRequest) (*saipb.CreateVlanResponse, error) { if _, ok := vlan.oidByVId[r.GetVlanId()]; ok { return nil, fmt.Errorf("found existing VLAN %d", r.GetVlanId()) diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index a277952b..3c08fc80 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -30,7 +30,7 @@ import ( "github.com/openconfig/lemming/dataplane/dplaneopts" "github.com/openconfig/lemming/dataplane/forwarding/fwdconfig" - "github.com/openconfig/lemming/dataplane/forwarding/fwdtable/bridge" + fwdbridge "github.com/openconfig/lemming/dataplane/forwarding/fwdtable/bridge" "github.com/openconfig/lemming/dataplane/forwarding/infra/fwdcontext" "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" @@ -464,12 +464,19 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ if _, err := sw.dataplane.TableCreate(ctx, fdbReq); err != nil { return nil, err } - if fwdCtx, err := sw.dataplane.FindContext(&fwdpb.ContextId{Id: sw.dataplane.ID()}); err == nil { + if fwdCtx, err := sw.dataplane.FindContext(&fwdpb.ContextId{Id: sw.dataplane.ID()}); err == nil && fwdCtx != nil { if obj, err := fwdCtx.Objects.FindID(&fwdpb.ObjectId{Id: FDBTable}); err == nil { - if brTable, ok := obj.(*bridge.Table); ok { + if brTable, ok := obj.(*fwdbridge.Table); ok { brTable.SetLearnCallback(sw.onBridgeLearn) + slog.Info("CreateSwitch: successfully set learn callback on FDB table") + } else { + slog.Error("CreateSwitch: FDB table object is not a fwdbridge.Table", "type", fmt.Sprintf("%T", obj)) } + } else { + slog.Error("CreateSwitch: failed to find FDB table object", "err", err) } + } else { + slog.Error("CreateSwitch: failed to find forwarding context", "err", err, "fwdCtx", fwdCtx) } floodReq := &fwdpb.TableCreateRequest{ ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, @@ -1275,13 +1282,17 @@ func (sw *saiSwitch) PortStateChangeNotification(_ *saipb.PortStateChangeNotific } } -func (sw *saiSwitch) FdbEventNotification(_ *saipb.FdbEventNotificationRequest, srv grpc.ServerStreamingServer[saipb.FdbEventNotificationResponse]) error { +func (sw *saiSwitch) FdbEventNotification(req *saipb.FdbEventNotificationRequest, srv grpc.ServerStreamingServer[saipb.FdbEventNotificationResponse]) error { if sw.fdb == nil { return status.Error(codes.Unimplemented, "FDB notification service unavailable") } + slog.InfoContext(srv.Context(), "FdbEventNotification: subscriber connected", "req", req) ch := make(chan *saipb.FdbEventNotificationResponse, 100) unsubscribe := sw.fdb.subscribe(ch) - defer unsubscribe() + defer func() { + unsubscribe() + slog.InfoContext(srv.Context(), "FdbEventNotification: subscriber disconnected", "err", srv.Context().Err()) + }() for { select { @@ -1289,9 +1300,12 @@ func (sw *saiSwitch) FdbEventNotification(_ *saipb.FdbEventNotificationRequest, return srv.Context().Err() case resp, ok := <-ch: if !ok { + slog.InfoContext(srv.Context(), "FdbEventNotification: subscription channel closed") return nil } + slog.InfoContext(srv.Context(), "FdbEventNotification: sending event", "resp", resp) if err := srv.Send(resp); err != nil { + slog.ErrorContext(srv.Context(), "FdbEventNotification: failed to send event", "err", err) return err } } @@ -1307,19 +1321,84 @@ func (sw *saiSwitch) onBridgeLearn(mac []byte, portID string) { slog.Warn("onBridgeLearn: failed to parse numeric port ID", "portID", portID, "err", err) return } + + // Find the Bridge Port OID that wraps this physical port. + var bridgePortOID uint64 + bpOIDs := sw.mgr.GetOIDsByType(saipb.ObjectType_OBJECT_TYPE_BRIDGE_PORT) + for _, bpOID := range bpOIDs { + val := sw.mgr.GetAttribute(strconv.FormatUint(bpOID, 10), int32(saipb.BridgePortAttr_BRIDGE_PORT_ATTR_PORT_ID)) + if val != nil { + if pID, ok := val.(uint64); ok && pID == portNum { + bridgePortOID = bpOID + break + } + } + } + + if bridgePortOID == 0 { + slog.Warn("onBridgeLearn: failed to find bridge port for physical port", "portNum", portNum) + return + } + + // Find the VLAN ID and VLAN OID for this port. + vlanID := uint32(DefaultVlanId) + var vlanOID uint64 + if sw.vlan != nil { + if member := sw.vlan.memberByPortId(portNum); member != nil { + vlanID = member.Vid + if oid, ok := sw.vlan.oidByVid(vlanID); ok { + vlanOID = oid + slog.Info("onBridgeLearn: found VLAN for port", "portNum", portNum, "vlanID", vlanID, "vlanOID", vlanOID) + } else { + slog.Warn("onBridgeLearn: failed to get OID for VLAN", "vlanID", vlanID) + } + } else { + slog.Warn("onBridgeLearn: no VLAN member found for port", "portNum", portNum) + } + } else { + slog.Warn("onBridgeLearn: vlan server is nil, using default vlan") + } + + swIDStr, ok := sw.mgr.GetSwitchID() + if !ok { + slog.Warn("onBridgeLearn: failed to get switch ID") + return + } + swID, err := strconv.ParseUint(swIDStr, 10, 64) + if err != nil { + slog.Warn("onBridgeLearn: failed to parse switch ID", "swIDStr", swIDStr, "err", err) + return + } + + // If we couldn't resolve the VLAN OID, fallback to default VLAN OID. + if vlanOID == 0 { + req := &saipb.GetSwitchAttributeRequest{Oid: swID, AttrType: []saipb.SwitchAttr{saipb.SwitchAttr_SWITCH_ATTR_DEFAULT_VLAN_ID}} + resp := &saipb.GetSwitchAttributeResponse{} + if err := sw.mgr.PopulateAttributes(req, resp); err == nil { + vlanOID = resp.GetAttr().GetDefaultVlanId() + slog.Info("onBridgeLearn: fallback to default VLAN", "defaultVlanOID", vlanOID) + } else { + slog.Error("onBridgeLearn: failed to get default VLAN OID", "err", err) + } + } + + slog.Info("onBridgeLearn: successfully translated and sending FdbEventNotification", "mac", mac, "portNum", portNum, "bridgePortOID", bridgePortOID, "vlanOID", vlanOID) sw.fdb.sendNotification(&saipb.FdbEventNotificationData{ EventType: saipb.FdbEvent_FDB_EVENT_LEARNED, FdbEntry: &saipb.FdbEntry{ + SwitchId: swID, MacAddress: mac, + BvId: vlanOID, }, Attrs: []*saipb.FdbEntryAttribute{ { - AttributeId: proto.Int32(int32(saipb.FdbEntryAttr_FDB_ENTRY_ATTR_BRIDGE_PORT_ID)), - Value: &saipb.AttributeValue{ - Value: &saipb.AttributeValue_Oid{ - Oid: portNum, - }, - }, + BridgePortId: proto.Uint64(bridgePortOID), + }, + { + Type: saipb.FdbEntryType_FDB_ENTRY_TYPE_DYNAMIC.Enum(), + }, + { + PacketAction: saipb.PacketAction_PACKET_ACTION_FORWARD.Enum(), }, }, }) diff --git a/dataplane/standalone/sai/common.cc b/dataplane/standalone/sai/common.cc index 798b977a..52e7e6a7 100644 --- a/dataplane/standalone/sai/common.cc +++ b/dataplane/standalone/sai/common.cc @@ -320,4 +320,99 @@ sai_acl_field_data_t convert_to_acl_field_data_ip_type( out.enable = in.enable(); out.data.s32 = convert_sai_acl_ip_type_t_to_sai(type); return out; -} \ No newline at end of file +} +lemming::dataplane::sai::FdbEntry convert_from_fdb_entry( + const sai_fdb_entry_t& entry) { + lemming::dataplane::sai::FdbEntry proto; + proto.set_switch_id(entry.switch_id); + proto.set_mac_address(entry.mac_address, sizeof(sai_mac_t)); + proto.set_bv_id(entry.bv_id); + return proto; +} + +sai_fdb_entry_t convert_to_fdb_entry( + const lemming::dataplane::sai::FdbEntry& proto) { + sai_fdb_entry_t entry; + entry.switch_id = proto.switch_id(); + if (proto.mac_address().size() == sizeof(sai_mac_t)) { + memcpy(entry.mac_address, proto.mac_address().data(), sizeof(sai_mac_t)); + } else { + memset(entry.mac_address, 0, sizeof(sai_mac_t)); + } + entry.bv_id = proto.bv_id(); + return entry; +} + +std::vector convert_to_fdb_attributes( + const google::protobuf::RepeatedPtrField& proto_attrs) { + std::vector attrs; + for (const auto& proto_attr : proto_attrs) { + if (proto_attr.has_type()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_TYPE; + attr.value.s32 = convert_sai_fdb_entry_type_t_to_sai(proto_attr.type()); + attrs.push_back(attr); + } + if (proto_attr.has_packet_action()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_PACKET_ACTION; + attr.value.s32 = convert_sai_packet_action_t_to_sai(proto_attr.packet_action()); + attrs.push_back(attr); + } + if (proto_attr.has_user_trap_id()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_USER_TRAP_ID; + attr.value.oid = proto_attr.user_trap_id(); + attrs.push_back(attr); + } + if (proto_attr.has_bridge_port_id()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_BRIDGE_PORT_ID; + attr.value.oid = proto_attr.bridge_port_id(); + attrs.push_back(attr); + } + if (proto_attr.has_meta_data()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_META_DATA; + attr.value.u32 = proto_attr.meta_data(); + attrs.push_back(attr); + } + if (proto_attr.has_endpoint_ip()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_ENDPOINT_IP; + attr.value.ipaddr = convert_to_ip_address(proto_attr.endpoint_ip()); + attrs.push_back(attr); + } + if (proto_attr.has_counter_id()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_COUNTER_ID; + attr.value.oid = proto_attr.counter_id(); + attrs.push_back(attr); + } + if (proto_attr.has_allow_mac_move()) { + sai_attribute_t attr; + attr.id = SAI_FDB_ENTRY_ATTR_ALLOW_MAC_MOVE; + attr.value.booldata = proto_attr.allow_mac_move(); + attrs.push_back(attr); + } + } + return attrs; +} + +FdbNotificationDataHolder convert_to_fdb_event( + const lemming::dataplane::sai::FdbEventNotificationResponse& resp) { + FdbNotificationDataHolder holder; + for (const auto& d : resp.data()) { + sai_fdb_event_notification_data_t event; + event.event_type = convert_sai_fdb_event_t_to_sai(d.event_type()); + event.fdb_entry = convert_to_fdb_entry(d.fdb_entry()); + + holder.attributes_storage.push_back(convert_to_fdb_attributes(d.attrs())); + + event.attr_count = holder.attributes_storage.back().size(); + event.attr = holder.attributes_storage.back().data(); + + holder.events.push_back(event); + } + return holder; +} diff --git a/dataplane/standalone/sai/common.h b/dataplane/standalone/sai/common.h index 2ed7b09e..abca31e4 100644 --- a/dataplane/standalone/sai/common.h +++ b/dataplane/standalone/sai/common.h @@ -150,12 +150,22 @@ sai_ip_prefix_t convert_to_ip_prefix( const lemming::dataplane::sai::IpPrefix& ip_prefix); std::vector convert_to_oper_status( const lemming::dataplane::sai::PortStateChangeNotificationResponse& resp); +struct FdbNotificationDataHolder { + std::vector events; + std::vector> attributes_storage; +}; +FdbNotificationDataHolder convert_to_fdb_event( + const lemming::dataplane::sai::FdbEventNotificationResponse& resp); lemming::dataplane::sai::NeighborEntry convert_from_neighbor_entry( const sai_neighbor_entry_t& entry); sai_neighbor_entry_t convert_to_neighbor_entry( const lemming::dataplane::sai::NeighborEntry& entry); +lemming::dataplane::sai::FdbEntry convert_from_fdb_entry( + const sai_fdb_entry_t& entry); +sai_fdb_entry_t convert_to_fdb_entry( + const lemming::dataplane::sai::FdbEntry& entry); void convert_to_acl_capability( sai_acl_capability_t& out, @@ -316,4 +326,76 @@ class PortStateReactor }; #endif + +#ifndef GRPC_CALLBACK_API_NONEXPERIMENTAL +class FdbEventReactor + : public grpc::experimental::ClientReadReactor< + lemming::dataplane::sai::FdbEventNotificationResponse> { + public: + FdbEventReactor(std::shared_ptr stub, + sai_fdb_event_notification_fn callback) { + this->callback = callback; + lemming::dataplane::sai::FdbEventNotificationRequest req; + stub->experimental_async()->FdbEventNotification(&context, &req, this); + StartRead(&resp); + StartCall(); + } + + void OnReadDone(bool ok) override { + if (!ok) return; + LOG(INFO) << "FdbEventReactor: OnReadDone received " << resp.data_size() << " event(s). Invoking callback."; + FdbNotificationDataHolder holder = convert_to_fdb_event(resp); + callback(holder.events.size(), holder.events.data()); + StartRead(&resp); + } + + void OnDone(const grpc::Status& status) override { + if (status.ok()) { + LOG(INFO) << "FdbEventNotification RPC succeeded."; + } else { + LOG(ERROR) << "FdbEventNotification RPC failed: " << status.error_message(); + } + } + + private: + grpc::ClientContext context; + lemming::dataplane::sai::FdbEventNotificationResponse resp; + sai_fdb_event_notification_fn callback; +}; +#else +class FdbEventReactor + : public grpc::ClientReadReactor< + lemming::dataplane::sai::FdbEventNotificationResponse> { + public: + FdbEventReactor(std::shared_ptr stub, + sai_fdb_event_notification_fn callback) { + this->callback = callback; + lemming::dataplane::sai::FdbEventNotificationRequest req; + stub->async()->FdbEventNotification(&context, &req, this); + StartRead(&resp); + StartCall(); + } + + void OnReadDone(bool ok) override { + if (!ok) return; + LOG(INFO) << "FdbEventReactor: OnReadDone received " << resp.data_size() << " event(s). Invoking callback."; + FdbNotificationDataHolder holder = convert_to_fdb_event(resp); + callback(holder.events.size(), holder.events.data()); + StartRead(&resp); + } + + void OnDone(const grpc::Status& status) override { + if (status.ok()) { + LOG(INFO) << "FdbEventNotification RPC succeeded."; + } else { + LOG(ERROR) << "FdbEventNotification RPC failed: " << status.error_message(); + } + } + + private: + grpc::ClientContext context; + lemming::dataplane::sai::FdbEventNotificationResponse resp; + sai_fdb_event_notification_fn callback; +}; +#endif #endif // DATAPLANE_STANDALONE_SAI_COMMON_H_ diff --git a/dataplane/standalone/sai/switch.cc b/dataplane/standalone/sai/switch.cc index bbb37106..ebcebb49 100644 --- a/dataplane/standalone/sai/switch.cc +++ b/dataplane/standalone/sai/switch.cc @@ -36,6 +36,7 @@ const sai_switch_api_t l_switch = { }; std::unique_ptr port_state; +std::unique_ptr fdb_event; lemming::dataplane::sai::CreateSwitchRequest convert_create_switch( uint32_t attr_count, const sai_attribute_t* attr_list) { @@ -182,6 +183,11 @@ lemming::dataplane::sai::CreateSwitchRequest convert_create_switch( switch_, reinterpret_cast( attr_list[i].value.ptr)); break; + case SAI_SWITCH_ATTR_FDB_EVENT_NOTIFY: + fdb_event = std::make_unique( + switch_, reinterpret_cast( + attr_list[i].value.ptr)); + break; case SAI_SWITCH_ATTR_FAST_API_ENABLE: msg.set_fast_api_enable(attr_list[i].value.booldata); break; @@ -441,6 +447,10 @@ lemming::dataplane::sai::CreateSwitchTunnelRequest convert_create_switch_tunnel( sai_status_t l_create_switch(sai_object_id_t* switch_id, uint32_t attr_count, const sai_attribute_t* attr_list) { LOG(INFO) << "Func: " << __PRETTY_FUNCTION__; + LOG(INFO) << "Compiled SAI_SWITCH_ATTR_FDB_EVENT_NOTIFY ID = " << SAI_SWITCH_ATTR_FDB_EVENT_NOTIFY; + for (uint32_t i = 0; i < attr_count; i++) { + LOG(INFO) << "l_create_switch attr ID=" << attr_list[i].id << " (0x" << std::hex << attr_list[i].id << std::dec << ") ptr=" << attr_list[i].value.ptr; + } lemming::dataplane::sai::CreateSwitchRequest req = convert_create_switch(attr_count, attr_list); @@ -619,7 +629,16 @@ sai_status_t l_set_switch_attribute(sai_object_id_t switch_id, port_state = std::make_unique( switch_, reinterpret_cast( attr->value.ptr)); - break; + return SAI_STATUS_SUCCESS; + case SAI_SWITCH_ATTR_FDB_EVENT_NOTIFY: + fdb_event = std::make_unique( + switch_, reinterpret_cast( + attr->value.ptr)); + return SAI_STATUS_SUCCESS; + case SAI_SWITCH_ATTR_SHUTDOWN_REQUEST_NOTIFY: + case SAI_SWITCH_ATTR_SWITCH_STATE_CHANGE_NOTIFY: + case SAI_SWITCH_ATTR_PACKET_EVENT_NOTIFY: + return SAI_STATUS_SUCCESS; case SAI_SWITCH_ATTR_FAST_API_ENABLE: req.set_fast_api_enable(attr->value.booldata); break; From ddd8fc56a502fa62aa4c0c650b1538c0d9aefa61 Mon Sep 17 00:00:00 2001 From: thineshvs Date: Fri, 21 Aug 2026 13:15:58 +0000 Subject: [PATCH 5/7] fix(dataplane): register SAI object types for bridge ports, VLANs, and entrypoint stubs (checkpoint 1) --- dataplane/saiserver/routing.go | 4 ++++ dataplane/standalone/entrypoint.cc | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/dataplane/saiserver/routing.go b/dataplane/saiserver/routing.go index b5ed8b9b..f52d1bb8 100644 --- a/dataplane/saiserver/routing.go +++ b/dataplane/saiserver/routing.go @@ -1274,6 +1274,7 @@ func (vlan *vlan) CreateVlan(ctx context.Context, r *saipb.CreateVlanRequest) (* return nil, fmt.Errorf("found existing VLAN %d", r.GetVlanId()) } id := vlan.mgr.NextID() + vlan.mgr.SetType(fmt.Sprint(id), saipb.ObjectType_OBJECT_TYPE_VLAN) req := &saipb.GetSwitchAttributeRequest{Oid: 1, AttrType: []saipb.SwitchAttr{saipb.SwitchAttr_SWITCH_ATTR_DEFAULT_STP_INST_ID}} resp := &saipb.GetSwitchAttributeResponse{} @@ -1398,6 +1399,7 @@ func (vlan *vlan) CreateVlanMember(ctx context.Context, r *saipb.CreateVlanMembe member := vlan.memberByPortId(portID) mOid := vlan.mgr.NextID() + vlan.mgr.SetType(fmt.Sprint(mOid), saipb.ObjectType_OBJECT_TYPE_VLAN_MEMBER) nid, err := vlan.dataplane.ObjectNID(ctx, &fwdpb.ObjectNIDRequest{ ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, ObjectId: &fwdpb.ObjectId{Id: fmt.Sprint(portID)}, @@ -1549,6 +1551,7 @@ func newBridge(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Serve func (b *bridge) CreateBridge(ctx context.Context, req *saipb.CreateBridgeRequest) (*saipb.CreateBridgeResponse, error) { id := b.mgr.NextID() + b.mgr.SetType(fmt.Sprint(id), saipb.ObjectType_OBJECT_TYPE_BRIDGE) attrs := &saipb.BridgeAttribute{ PortList: []uint64{}, UnknownUnicastFloodGroup: proto.Uint64(0), @@ -1579,6 +1582,7 @@ func (b *bridge) GetBridgeStats(ctx context.Context, req *saipb.GetBridgeStatsRe func (b *bridge) CreateBridgePort(ctx context.Context, req *saipb.CreateBridgePortRequest) (*saipb.CreateBridgePortResponse, error) { oid := b.mgr.NextID() + b.mgr.SetType(fmt.Sprint(oid), saipb.ObjectType_OBJECT_TYPE_BRIDGE_PORT) adminState := req.GetAdminState() attrs := &saipb.BridgePortAttribute{ AdminState: proto.Bool(adminState), diff --git a/dataplane/standalone/entrypoint.cc b/dataplane/standalone/entrypoint.cc index 19f536ac..2125e485 100644 --- a/dataplane/standalone/entrypoint.cc +++ b/dataplane/standalone/entrypoint.cc @@ -33,6 +33,7 @@ #include "dataplane/proto/sai/dtel.grpc.pb.h" #include "dataplane/proto/sai/fdb.grpc.pb.h" #include "dataplane/proto/sai/generic_programmable.grpc.pb.h" +#include "dataplane/proto/sai/icmp_echo.grpc.pb.h" #include "dataplane/proto/sai/hash.grpc.pb.h" #include "dataplane/proto/sai/hostif.grpc.pb.h" #include "dataplane/proto/sai/ipmc.grpc.pb.h" @@ -83,6 +84,7 @@ #include "dataplane/standalone/sai/generic_programmable.h" #include "dataplane/standalone/sai/hash.h" #include "dataplane/standalone/sai/hostif.h" +#include "dataplane/standalone/sai/icmp_echo.h" #include "dataplane/standalone/sai/ipmc.h" #include "dataplane/standalone/sai/ipmc_group.h" #include "dataplane/standalone/sai/ipsec.h" @@ -138,6 +140,7 @@ std::unique_ptr dtel; std::unique_ptr fdb; std::unique_ptr hash; std::unique_ptr hostif; +std::unique_ptr icmp_echo; std::unique_ptr ipmc_group; std::unique_ptr ipmc; std::unique_ptr ipsec; @@ -203,6 +206,7 @@ sai_status_t sai_api_initialize( fdb = std::make_unique(chan); hash = std::make_unique(chan); hostif = std::make_unique(chan); + icmp_echo = std::make_unique(chan); ipmc_group = std::make_unique(chan); ipmc = std::make_unique(chan); ipsec = std::make_unique(chan); @@ -463,6 +467,10 @@ sai_status_t sai_api_query(_In_ sai_api_t api, _Out_ void **api_method_table) { *api_method_table = const_cast(&l_bmtor); break; } + case SAI_API_ICMP_ECHO: { + *api_method_table = const_cast(&l_icmp_echo); + break; + } default: LOG(WARNING) << "unknown API type " << api; return SAI_STATUS_NOT_IMPLEMENTED; @@ -489,6 +497,25 @@ sai_object_type_t sai_object_type_query(_In_ sai_object_id_t object_id) { return static_cast(resp.type() - 1); } +sai_object_id_t sai_switch_id_query(_In_ sai_object_id_t object_id) { + if (object_id == SAI_NULL_OBJECT_ID) { + return SAI_NULL_OBJECT_ID; + } + return 1; +} + +sai_status_t sai_query_api_version(_Out_ sai_api_version_t *version) { + if (!version) { + return SAI_STATUS_INVALID_PARAMETER; + } +#ifdef SAI_API_VERSION + *version = SAI_API_VERSION; +#else + *version = 1010500; +#endif + return SAI_STATUS_SUCCESS; +} + sai_status_t sai_query_attribute_capability( _In_ sai_object_id_t switch_id, _In_ sai_object_type_t object_type, _In_ sai_attr_id_t attr_id, _Out_ sai_attr_capability_t *attr_capability) { From a5c31caa840c4a9f67a6ba6abb75e824876b31ff Mon Sep 17 00:00:00 2001 From: thineshvs Date: Fri, 21 Aug 2026 13:43:05 +0000 Subject: [PATCH 6/7] feat(dataplane): implement FlushFdbEntries, dynamic/static FDB tracking and aging events --- dataplane/saiserver/BUILD | 1 + dataplane/saiserver/fdb.go | 187 ++++++++++++++++++++++++- dataplane/saiserver/fdb_test.go | 232 ++++++++++++++++++++++++++++++++ dataplane/saiserver/switch.go | 3 + 4 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 dataplane/saiserver/fdb_test.go diff --git a/dataplane/saiserver/BUILD b/dataplane/saiserver/BUILD index a7837e76..b37946e3 100644 --- a/dataplane/saiserver/BUILD +++ b/dataplane/saiserver/BUILD @@ -46,6 +46,7 @@ go_test( srcs = [ "acl_test.go", "bridge_test.go", + "fdb_test.go", "hostif_test.go", "l2mc_test.go", "mirror_test.go", diff --git a/dataplane/saiserver/fdb.go b/dataplane/saiserver/fdb.go index 256dfffc..4aed1a66 100644 --- a/dataplane/saiserver/fdb.go +++ b/dataplane/saiserver/fdb.go @@ -26,17 +26,32 @@ import ( "google.golang.org/protobuf/proto" "github.com/openconfig/lemming/dataplane/forwarding/fwdconfig" + fwdbridge "github.com/openconfig/lemming/dataplane/forwarding/fwdtable/bridge" saipb "github.com/openconfig/lemming/dataplane/proto/sai" "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" fwdpb "github.com/openconfig/lemming/proto/forwarding" ) +type fdbEntryKey struct { + bvID uint64 + mac string +} + +type fdbEntryRecord struct { + mac []byte + bvID uint64 + bridgePortID uint64 + entryType saipb.FdbEntryType + switchID uint64 +} + type fdb struct { saipb.UnimplementedFdbServer mgr *attrmgr.AttrMgr dataplane switchDataplaneAPI mu sync.RWMutex subscribers map[chan *saipb.FdbEventNotificationResponse]struct{} + entries map[fdbEntryKey]*fdbEntryRecord } func newFdb(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server) (*fdb, error) { @@ -44,6 +59,7 @@ func newFdb(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server) mgr: mgr, dataplane: dataplane, subscribers: make(map[chan *saipb.FdbEventNotificationResponse]struct{}), + entries: make(map[fdbEntryKey]*fdbEntryRecord), } saipb.RegisterFdbServer(s, f) return f, nil @@ -61,11 +77,48 @@ func (f *fdb) subscribe(ch chan *saipb.FdbEventNotificationResponse) func() { } func (f *fdb) sendNotification(data *saipb.FdbEventNotificationData) { - f.mu.RLock() - defer f.mu.RUnlock() + if data == nil || data.GetFdbEntry() == nil { + return + } + f.mu.Lock() + if f.entries == nil { + f.entries = make(map[fdbEntryKey]*fdbEntryRecord) + } + key := fdbEntryKey{ + bvID: data.GetFdbEntry().GetBvId(), + mac: string(data.GetFdbEntry().GetMacAddress()), + } + switch data.GetEventType() { + case saipb.FdbEvent_FDB_EVENT_LEARNED: + rec := &fdbEntryRecord{ + mac: data.GetFdbEntry().GetMacAddress(), + bvID: data.GetFdbEntry().GetBvId(), + switchID: data.GetFdbEntry().GetSwitchId(), + entryType: saipb.FdbEntryType_FDB_ENTRY_TYPE_DYNAMIC, + } + for _, attr := range data.GetAttrs() { + if attr.BridgePortId != nil { + rec.bridgePortID = attr.GetBridgePortId() + } + if attr.Type != nil { + rec.entryType = attr.GetType() + } + } + f.entries[key] = rec + case saipb.FdbEvent_FDB_EVENT_AGED, saipb.FdbEvent_FDB_EVENT_FLUSHED: + delete(f.entries, key) + } + f.mu.Unlock() + + f.broadcastNotification(data) +} + +func (f *fdb) broadcastNotification(data *saipb.FdbEventNotificationData) { resp := &saipb.FdbEventNotificationResponse{ Data: []*saipb.FdbEventNotificationData{data}, } + f.mu.RLock() + defer f.mu.RUnlock() for ch := range f.subscribers { select { case ch <- resp: @@ -76,6 +129,72 @@ func (f *fdb) sendNotification(data *saipb.FdbEventNotificationData) { } func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesRequest) (*saipb.FlushFdbEntriesResponse, error) { + slog.InfoContext(ctx, "FlushFdbEntries called", "bridgePortId", req.GetBridgePortId(), "bvId", req.GetBvId(), "entryType", req.GetEntryType()) + + f.mu.Lock() + var toFlush []*fdbEntryRecord + for key, entry := range f.entries { + if req.GetBridgePortId() != 0 && entry.bridgePortID != req.GetBridgePortId() { + continue + } + if req.GetBvId() != 0 && entry.bvID != req.GetBvId() { + continue + } + switch req.GetEntryType() { + case saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_DYNAMIC: + if entry.entryType != saipb.FdbEntryType_FDB_ENTRY_TYPE_DYNAMIC { + continue + } + case saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_STATIC: + if entry.entryType != saipb.FdbEntryType_FDB_ENTRY_TYPE_STATIC { + continue + } + } + toFlush = append(toFlush, entry) + delete(f.entries, key) + } + f.mu.Unlock() + + for _, entry := range toFlush { + delReq := fwdconfig.TableEntryRemoveRequest(f.dataplane.ID(), FDBTable).AppendEntry( + fwdconfig.EntryDesc(fwdconfig.ExactEntry( + fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST).WithBytes(entry.mac), + )), + ).Build() + + if _, err := f.dataplane.TableEntryRemove(ctx, delReq); err != nil { + slog.WarnContext(ctx, "FlushFdbEntries: failed to remove entry from dataplane", "mac", fmt.Sprintf("%x", entry.mac), "err", err) + } + + f.broadcastNotification(&saipb.FdbEventNotificationData{ + EventType: saipb.FdbEvent_FDB_EVENT_AGED, + FdbEntry: &saipb.FdbEntry{ + SwitchId: entry.switchID, + MacAddress: entry.mac, + BvId: entry.bvID, + }, + Attrs: []*saipb.FdbEntryAttribute{ + { + BridgePortId: proto.Uint64(entry.bridgePortID), + }, + { + Type: entry.entryType.Enum(), + }, + }, + }) + } + + // If flushing all entries (or all dynamic entries across the whole switch), also ensure bridge table is cleared. + if req.GetBridgePortId() == 0 && req.GetBvId() == 0 && (req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_ALL || req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_UNSPECIFIED) { + if fwdCtx, err := f.dataplane.FindContext(&fwdpb.ContextId{Id: f.dataplane.ID()}); err == nil && fwdCtx != nil { + if obj, err := fwdCtx.Objects.FindID(&fwdpb.ObjectId{Id: FDBTable}); err == nil { + if brTable, ok := obj.(*fwdbridge.Table); ok { + brTable.Clear() + } + } + } + } + return &saipb.FlushFdbEntriesResponse{}, nil } @@ -121,6 +240,11 @@ func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryReque return nil, fmt.Errorf("failed to add FDB entry to dataplane: %v", err) } + entryType := req.GetType() + if entryType == saipb.FdbEntryType_FDB_ENTRY_TYPE_UNSPECIFIED { + entryType = saipb.FdbEntryType_FDB_ENTRY_TYPE_STATIC + } + f.sendNotification(&saipb.FdbEventNotificationData{ EventType: saipb.FdbEvent_FDB_EVENT_LEARNED, FdbEntry: &saipb.FdbEntry{ @@ -132,6 +256,12 @@ func (f *fdb) CreateFdbEntry(ctx context.Context, req *saipb.CreateFdbEntryReque { BridgePortId: proto.Uint64(portOID), }, + { + Type: entryType.Enum(), + }, + { + PacketAction: req.GetPacketAction().Enum(), + }, }, }) @@ -160,6 +290,21 @@ func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryReque return nil, fmt.Errorf("failed to remove FDB entry from dataplane: %v", err) } + key := fdbEntryKey{ + bvID: entry.GetBvId(), + mac: string(mac), + } + f.mu.RLock() + rec := f.entries[key] + f.mu.RUnlock() + + var bpID uint64 + var entryType *saipb.FdbEntryType + if rec != nil { + bpID = rec.bridgePortID + entryType = rec.entryType.Enum() + } + f.sendNotification(&saipb.FdbEventNotificationData{ EventType: saipb.FdbEvent_FDB_EVENT_AGED, FdbEntry: &saipb.FdbEntry{ @@ -167,7 +312,45 @@ func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryReque MacAddress: mac, BvId: entry.GetBvId(), }, + Attrs: []*saipb.FdbEntryAttribute{ + { + BridgePortId: proto.Uint64(bpID), + }, + { + Type: entryType, + }, + }, }) return &saipb.RemoveFdbEntryResponse{}, nil } + +func (f *fdb) CreateFdbEntries(ctx context.Context, req *saipb.CreateFdbEntriesRequest) (*saipb.CreateFdbEntriesResponse, error) { + resp := &saipb.CreateFdbEntriesResponse{} + for _, r := range req.GetReqs() { + entryResp, err := f.CreateFdbEntry(ctx, r) + if err != nil { + return nil, err + } + resp.Resps = append(resp.Resps, entryResp) + } + return resp, nil +} + +func (f *fdb) RemoveFdbEntries(ctx context.Context, req *saipb.RemoveFdbEntriesRequest) (*saipb.RemoveFdbEntriesResponse, error) { + resp := &saipb.RemoveFdbEntriesResponse{} + for _, r := range req.GetReqs() { + entryResp, err := f.RemoveFdbEntry(ctx, r) + if err != nil { + return nil, err + } + resp.Resps = append(resp.Resps, entryResp) + } + return resp, nil +} + +func (f *fdb) Reset() { + f.mu.Lock() + defer f.mu.Unlock() + f.entries = make(map[fdbEntryKey]*fdbEntryRecord) +} diff --git a/dataplane/saiserver/fdb_test.go b/dataplane/saiserver/fdb_test.go new file mode 100644 index 00000000..bb801bf1 --- /dev/null +++ b/dataplane/saiserver/fdb_test.go @@ -0,0 +1,232 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package saiserver + +import ( + "bytes" + "context" + "testing" + + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + + "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" + saipb "github.com/openconfig/lemming/dataplane/proto/sai" +) + +func newTestFdb(t testing.TB, api switchDataplaneAPI) (saipb.FdbClient, *fdb, *attrmgr.AttrMgr, func()) { + var fdbServer *fdb + conn, mgr, stopFn := newTestServer(t, func(mgr *attrmgr.AttrMgr, srv *grpc.Server) { + var err error + fdbServer, err = newFdb(mgr, api, srv) + if err != nil { + t.Fatalf("newFdb failed: %v", err) + } + }) + return saipb.NewFdbClient(conn), fdbServer, mgr, stopFn +} + +func TestCreateAndRemoveFdbEntry(t *testing.T) { + dplane := &fakeSwitchDataplane{} + c, fdbSrv, mgr, stopFn := newTestFdb(t, dplane) + defer stopFn() + ctx := context.Background() + + // Subscribe to notifications. + notifCh := make(chan *saipb.FdbEventNotificationResponse, 10) + unsub := fdbSrv.subscribe(notifCh) + defer unsub() + + // Set up bridge port attribute. + bpOID := uint64(10) + mgr.StoreAttributes(bpOID, &saipb.BridgePortAttribute{ + PortId: proto.Uint64(1), + }) + + mac := []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55} + bvID := uint64(20) + + // Create FDB Entry. + _, err := c.CreateFdbEntry(ctx, &saipb.CreateFdbEntryRequest{ + Entry: &saipb.FdbEntry{ + SwitchId: 1, + MacAddress: mac, + BvId: bvID, + }, + BridgePortId: proto.Uint64(bpOID), + Type: saipb.FdbEntryType_FDB_ENTRY_TYPE_STATIC.Enum(), + PacketAction: saipb.PacketAction_PACKET_ACTION_FORWARD.Enum(), + }) + if err != nil { + t.Fatalf("CreateFdbEntry() failed: %v", err) + } + + // Verify notification. + select { + case notif := <-notifCh: + if len(notif.GetData()) != 1 { + t.Fatalf("Expected 1 notification item, got %d", len(notif.GetData())) + } + data := notif.GetData()[0] + if data.GetEventType() != saipb.FdbEvent_FDB_EVENT_LEARNED { + t.Errorf("Expected LEARNED event, got %v", data.GetEventType()) + } + if !bytes.Equal(data.GetFdbEntry().GetMacAddress(), mac) { + t.Errorf("Expected MAC %x, got %x", mac, data.GetFdbEntry().GetMacAddress()) + } + default: + t.Fatal("Expected FDB notification not received") + } + + // Remove FDB Entry. + _, err = c.RemoveFdbEntry(ctx, &saipb.RemoveFdbEntryRequest{ + Entry: &saipb.FdbEntry{ + SwitchId: 1, + MacAddress: mac, + BvId: bvID, + }, + }) + if err != nil { + t.Fatalf("RemoveFdbEntry() failed: %v", err) + } + + // Verify Aged/Removed notification. + select { + case notif := <-notifCh: + if len(notif.GetData()) != 1 { + t.Fatalf("Expected 1 notification item, got %d", len(notif.GetData())) + } + data := notif.GetData()[0] + if data.GetEventType() != saipb.FdbEvent_FDB_EVENT_AGED { + t.Errorf("Expected AGED event, got %v", data.GetEventType()) + } + default: + t.Fatal("Expected FDB aged notification not received") + } +} + +func TestFlushFdbEntries(t *testing.T) { + dplane := &fakeSwitchDataplane{} + c, fdbSrv, mgr, stopFn := newTestFdb(t, dplane) + defer stopFn() + ctx := context.Background() + + notifCh := make(chan *saipb.FdbEventNotificationResponse, 20) + unsub := fdbSrv.subscribe(notifCh) + defer unsub() + + mgr.StoreAttributes(10, &saipb.BridgePortAttribute{PortId: proto.Uint64(1)}) + mgr.StoreAttributes(20, &saipb.BridgePortAttribute{PortId: proto.Uint64(2)}) + mgr.StoreAttributes(30, &saipb.BridgePortAttribute{PortId: proto.Uint64(3)}) + + mac1 := []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55} + mac2 := []byte{0x00, 0x22, 0x33, 0x44, 0x55, 0x66} + mac3 := []byte{0x00, 0x33, 0x44, 0x55, 0x66, 0x77} + + // Simulate learned dynamic entry 1 on VLAN 20, port 10. + fdbSrv.sendNotification(&saipb.FdbEventNotificationData{ + EventType: saipb.FdbEvent_FDB_EVENT_LEARNED, + FdbEntry: &saipb.FdbEntry{ + SwitchId: 1, + MacAddress: mac1, + BvId: 20, + }, + Attrs: []*saipb.FdbEntryAttribute{ + {BridgePortId: proto.Uint64(10)}, + {Type: saipb.FdbEntryType_FDB_ENTRY_TYPE_DYNAMIC.Enum()}, + }, + }) + // Simulate learned dynamic entry 2 on VLAN 20, port 20. + fdbSrv.sendNotification(&saipb.FdbEventNotificationData{ + EventType: saipb.FdbEvent_FDB_EVENT_LEARNED, + FdbEntry: &saipb.FdbEntry{ + SwitchId: 1, + MacAddress: mac2, + BvId: 20, + }, + Attrs: []*saipb.FdbEntryAttribute{ + {BridgePortId: proto.Uint64(20)}, + {Type: saipb.FdbEntryType_FDB_ENTRY_TYPE_DYNAMIC.Enum()}, + }, + }) + // Create static entry 3 on VLAN 30, port 30. + _, err := c.CreateFdbEntry(ctx, &saipb.CreateFdbEntryRequest{ + Entry: &saipb.FdbEntry{ + SwitchId: 1, + MacAddress: mac3, + BvId: 30, + }, + BridgePortId: proto.Uint64(30), + Type: saipb.FdbEntryType_FDB_ENTRY_TYPE_STATIC.Enum(), + }) + if err != nil { + t.Fatalf("CreateFdbEntry failed: %v", err) + } + + // Drain setup notifications. + for len(notifCh) > 0 { + <-notifCh + } + + // Test 1: Flush dynamic entries on VLAN 20. + _, err = c.FlushFdbEntries(ctx, &saipb.FlushFdbEntriesRequest{ + Switch: 1, + BvId: proto.Uint64(20), + EntryType: saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_DYNAMIC.Enum(), + }) + if err != nil { + t.Fatalf("FlushFdbEntries failed: %v", err) + } + + // Should receive 2 AGED notifications for mac1 and mac2. + flushedMacs := make(map[string]bool) + for i := 0; i < 2; i++ { + select { + case notif := <-notifCh: + for _, d := range notif.GetData() { + if d.GetEventType() == saipb.FdbEvent_FDB_EVENT_AGED { + flushedMacs[string(d.GetFdbEntry().GetMacAddress())] = true + } + } + default: + t.Fatalf("Expected 2 flush notifications, got %d", len(flushedMacs)) + } + } + if !flushedMacs[string(mac1)] || !flushedMacs[string(mac2)] { + t.Errorf("Expected mac1 and mac2 to be flushed, got %v", flushedMacs) + } + + // Test 2: Flush all remaining entries. + _, err = c.FlushFdbEntries(ctx, &saipb.FlushFdbEntriesRequest{ + Switch: 1, + EntryType: saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_ALL.Enum(), + }) + if err != nil { + t.Fatalf("FlushFdbEntries(ALL) failed: %v", err) + } + + // Should receive 1 AGED notification for static mac3. + select { + case notif := <-notifCh: + if len(notif.GetData()) != 1 || notif.GetData()[0].GetEventType() != saipb.FdbEvent_FDB_EVENT_AGED { + t.Errorf("Expected mac3 flush notification, got %v", notif) + } + if !bytes.Equal(notif.GetData()[0].GetFdbEntry().GetMacAddress(), mac3) { + t.Errorf("Expected mac3 %x, got %x", mac3, notif.GetData()[0].GetFdbEntry().GetMacAddress()) + } + default: + t.Fatal("Expected mac3 flush notification not received") + } +} diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index 3c08fc80..b8778dfc 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -1408,6 +1408,9 @@ func (sw *saiSwitch) Reset() { sw.vlan.Reset() sw.port.Reset() sw.hostif.Reset() + if sw.fdb != nil { + sw.fdb.Reset() + } } // GetSwitchStats returns the statistics for the switch. From 9433fd498f42a22820279b173e57ec04204b02a2 Mon Sep 17 00:00:00 2001 From: thineshvs Date: Sun, 23 Aug 2026 12:57:44 +0000 Subject: [PATCH 7/7] feat(dataplane): dynamic FDB learn resolution, 802.1Q tag outputs, and SAI Bookworm build (checkpoint 2) - dataplane/saiserver/routing.go: - Dynamically set packet output decap pipeline on VLAN member ports based on VlanTaggingMode (TAGGED keeps 802.1Q header on wire; UNTAGGED decaps 802.1Q header). - Store BridgePortID in vlanMember struct for fast, reliable bridge port OID resolution. - dataplane/saiserver/switch.go: - Fixed onBridgeLearn bridge port resolution for all physical ports (including high port IDs) via member.BridgePortID and PopulateAttributes. - dataplane/saiserver/fdb.go & dataplane/forwarding/: - Fixed FlushFdbEntries and RemoveFdbEntry to synchronize state with internal fwdbridge.Table. - dataplane/standalone/sai/fdb.cc: - Added conversion from sai_fdb_entry_t to protobuf in SAI FDB entry API functions and bulk operations. - dataplane/standalone/BUILD & Dockerfile.saibuilder: - Added Debian 12 Bookworm container build definition with static libstdc++/libgcc linking for SONiC runtime. --- Dockerfile.saibuilder | 24 ++++++---- .../forwarding/fwdtable/bridge/bridge.go | 5 ++ dataplane/forwarding/fwdtable/exact/exact.go | 21 +++++++++ dataplane/saiserver/fdb.go | 36 +++++++++++---- dataplane/saiserver/routing.go | 39 ++++++++++++++-- dataplane/saiserver/switch.go | 46 +++++++++++-------- dataplane/standalone/BUILD | 4 ++ dataplane/standalone/sai/fdb.cc | 20 ++++++++ 8 files changed, 151 insertions(+), 44 deletions(-) diff --git a/Dockerfile.saibuilder b/Dockerfile.saibuilder index 3cf0670c..db87d01e 100644 --- a/Dockerfile.saibuilder +++ b/Dockerfile.saibuilder @@ -1,16 +1,20 @@ -FROM us-west1-docker.pkg.dev/openconfig-lemming/internal/builder@sha256:6a960d06bfd63c9cd8cff1f2ab3b3cc21e578b570979b5574d232be644dd0bf9 +FROM docker.io/debian:bookworm WORKDIR /build +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libprotobuf-dev \ + protobuf-compiler \ + libgrpc++-dev \ + protobuf-compiler-grpc \ + libgoogle-glog-dev \ + wget \ + patch \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* COPY patches/sai.patch sai.patch -RUN wget -q https://github.com/opencomputeproject/SAI/archive/refs/tags/v1.14.0.tar.gz && tar xf v1.14.0.tar.gz && rm v1.14.0.tar.gz -RUN mkdir external && mv SAI-1.14.0 external/com_github_opencomputeproject_sai && patch -p1 -d external/com_github_opencomputeproject_sai < sai.patch -COPY go.* . +RUN wget -q https://github.com/opencomputeproject/SAI/archive/refs/tags/v1.15.0.tar.gz && tar xf v1.15.0.tar.gz && rm v1.15.0.tar.gz +RUN mkdir external && mv SAI-1.15.0 external/com_github_opencomputeproject_sai && patch -p1 -d external/com_github_opencomputeproject_sai < sai.patch COPY dataplane/proto/sai/*.proto dataplane/proto/sai/ -COPY dataplane/cpusink/*.go dataplane/cpusink/ -COPY dataplane/standalone/packetio/*.go dataplane/standalone/packetio/ -COPY dataplane/dplaneopts/*.go dataplane/dplaneopts/ -COPY dataplane/forwarding/ dataplane/forwarding/ -COPY dataplane/internal/kernel/*.go dataplane/internal/kernel/ -COPY proto/forwarding/*.go proto/forwarding/ COPY dataplane/standalone/sai/*.cc dataplane/standalone/sai/ COPY dataplane/standalone/sai/*.h dataplane/standalone/sai/ COPY dataplane/standalone/entrypoint.cc dataplane/standalone/entrypoint.cc diff --git a/dataplane/forwarding/fwdtable/bridge/bridge.go b/dataplane/forwarding/fwdtable/bridge/bridge.go index ca06420b..a0b2b64d 100644 --- a/dataplane/forwarding/fwdtable/bridge/bridge.go +++ b/dataplane/forwarding/fwdtable/bridge/bridge.go @@ -87,6 +87,11 @@ func (t *Table) Clear() { t.Table.Clear() } +// Remove removes the entry matching the given MAC from the table. +func (t *Table) Remove(mac []byte) error { + return t.Table.RemoveKey(mac) +} + // Cleanup cleans up the exact match table and stops learning. func (t *Table) Cleanup() { t.learn.Close() diff --git a/dataplane/forwarding/fwdtable/exact/exact.go b/dataplane/forwarding/fwdtable/exact/exact.go index cb34796f..aa97cf15 100644 --- a/dataplane/forwarding/fwdtable/exact/exact.go +++ b/dataplane/forwarding/fwdtable/exact/exact.go @@ -154,6 +154,12 @@ type Table struct { func (t *Table) Clear() { t.entriesMu.Lock() defer t.entriesMu.Unlock() + if t.stale != nil { + t.staleMu.Lock() + t.stale.head = nil + t.stale.tail = nil + t.staleMu.Unlock() + } for pos, head := range t.entries { for entry := head; entry != nil; entry = entry.hashNext { entry.actions.Cleanup() @@ -307,6 +313,21 @@ func (t *Table) RemoveEntry(ed *fwdpb.EntryDesc) error { return nil } +// RemoveKey removes the entry associated with the given key. +func (t *Table) RemoveKey(key tableutil.Key) error { + entry := t.Find(key) + if entry == nil { + return fmt.Errorf("exact: RemoveKey failed, cannot find key %v", key) + } + if t.stale != nil && entry.transient { + t.staleMu.Lock() + t.stale.remove(entry) + t.staleMu.Unlock() + } + t.remove(entry) + return nil +} + // Entries lists all entries in a table. Note that the order of entries is // non-deterministic. func (t *Table) Entries() []string { diff --git a/dataplane/saiserver/fdb.go b/dataplane/saiserver/fdb.go index 4aed1a66..cc56794b 100644 --- a/dataplane/saiserver/fdb.go +++ b/dataplane/saiserver/fdb.go @@ -131,6 +131,13 @@ func (f *fdb) broadcastNotification(data *saipb.FdbEventNotificationData) { func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesRequest) (*saipb.FlushFdbEntriesResponse, error) { slog.InfoContext(ctx, "FlushFdbEntries called", "bridgePortId", req.GetBridgePortId(), "bvId", req.GetBvId(), "entryType", req.GetEntryType()) + var brTable *fwdbridge.Table + if fwdCtx, err := f.dataplane.FindContext(&fwdpb.ContextId{Id: f.dataplane.ID()}); err == nil && fwdCtx != nil { + if obj, err := fwdCtx.Objects.FindID(&fwdpb.ObjectId{Id: FDBTable}); err == nil { + brTable, _ = obj.(*fwdbridge.Table) + } + } + f.mu.Lock() var toFlush []*fdbEntryRecord for key, entry := range f.entries { @@ -156,6 +163,10 @@ func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesReq f.mu.Unlock() for _, entry := range toFlush { + if brTable != nil { + _ = brTable.Remove(entry.mac) + } + delReq := fwdconfig.TableEntryRemoveRequest(f.dataplane.ID(), FDBTable).AppendEntry( fwdconfig.EntryDesc(fwdconfig.ExactEntry( fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST).WithBytes(entry.mac), @@ -185,13 +196,9 @@ func (f *fdb) FlushFdbEntries(ctx context.Context, req *saipb.FlushFdbEntriesReq } // If flushing all entries (or all dynamic entries across the whole switch), also ensure bridge table is cleared. - if req.GetBridgePortId() == 0 && req.GetBvId() == 0 && (req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_ALL || req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_UNSPECIFIED) { - if fwdCtx, err := f.dataplane.FindContext(&fwdpb.ContextId{Id: f.dataplane.ID()}); err == nil && fwdCtx != nil { - if obj, err := fwdCtx.Objects.FindID(&fwdpb.ObjectId{Id: FDBTable}); err == nil { - if brTable, ok := obj.(*fwdbridge.Table); ok { - brTable.Clear() - } - } + if req.GetBridgePortId() == 0 && req.GetBvId() == 0 && (req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_ALL || req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_DYNAMIC || req.GetEntryType() == saipb.FdbFlushEntryType_FDB_FLUSH_ENTRY_TYPE_UNSPECIFIED) { + if brTable != nil { + brTable.Clear() } } @@ -280,6 +287,14 @@ func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryReque return nil, status.Errorf(codes.InvalidArgument, "MAC address is required") } + if fwdCtx, err := f.dataplane.FindContext(&fwdpb.ContextId{Id: f.dataplane.ID()}); err == nil && fwdCtx != nil { + if obj, err := fwdCtx.Objects.FindID(&fwdpb.ObjectId{Id: FDBTable}); err == nil { + if brTable, ok := obj.(*fwdbridge.Table); ok { + _ = brTable.Remove(mac) + } + } + } + delReq := fwdconfig.TableEntryRemoveRequest(f.dataplane.ID(), FDBTable).AppendEntry( fwdconfig.EntryDesc(fwdconfig.ExactEntry( fwdconfig.PacketFieldBytes(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST).WithBytes(mac), @@ -287,16 +302,17 @@ func (f *fdb) RemoveFdbEntry(ctx context.Context, req *saipb.RemoveFdbEntryReque ).Build() if _, err := f.dataplane.TableEntryRemove(ctx, delReq); err != nil { - return nil, fmt.Errorf("failed to remove FDB entry from dataplane: %v", err) + slog.WarnContext(ctx, "failed to remove FDB entry from dataplane", "err", err) } key := fdbEntryKey{ bvID: entry.GetBvId(), mac: string(mac), } - f.mu.RLock() + f.mu.Lock() rec := f.entries[key] - f.mu.RUnlock() + delete(f.entries, key) + f.mu.Unlock() var bpID uint64 var entryType *saipb.FdbEntryType diff --git a/dataplane/saiserver/routing.go b/dataplane/saiserver/routing.go index f52d1bb8..c50b5e37 100644 --- a/dataplane/saiserver/routing.go +++ b/dataplane/saiserver/routing.go @@ -1203,10 +1203,11 @@ func (ri *routerInterface) RemoveRouterInterfaces(ctx context.Context, req *saip // vlanMember contains the info of a VLAN member. type vlanMember struct { - Oid uint64 - PortID uint64 - Vid uint32 - Mode saipb.VlanTaggingMode + Oid uint64 + PortID uint64 + Vid uint32 + Mode saipb.VlanTaggingMode + BridgePortID uint64 } type vlan struct { @@ -1417,6 +1418,28 @@ func (vlan *vlan) CreateVlanMember(ctx context.Context, r *saipb.CreateVlanMembe if _, err := vlan.dataplane.TableEntryAdd(ctx, vlanReq); err != nil { return nil, err } + outputActions := []*fwdpb.ActionDesc{} + if r.GetVlanTaggingMode() == saipb.VlanTaggingMode_VLAN_TAGGING_MODE_UNTAGGED || r.GetVlanTaggingMode() == saipb.VlanTaggingMode_VLAN_TAGGING_MODE_PRIORITY_TAGGED { + outputActions = []*fwdpb.ActionDesc{ + fwdconfig.Action(fwdconfig.DecapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET_VLAN)).Build(), + } + } + portUpd := &fwdpb.PortUpdateRequest{ + ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: fmt.Sprint(portID)}}, + Update: &fwdpb.PortUpdateDesc{ + Port: &fwdpb.PortUpdateDesc_Kernel{ + Kernel: &fwdpb.KernelPortUpdateDesc{ + Inputs: getPreIngressPipeline(), + Outputs: outputActions, + }, + }, + }, + } + if _, err := vlan.dataplane.PortUpdate(ctx, portUpd); err != nil { + return nil, err + } + floodID := fmt.Sprintf("vlan_flood_%d", vId) _, _ = vlan.dataplane.PortUpdate(ctx, &fwdpb.PortUpdateRequest{ ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, @@ -1439,7 +1462,13 @@ func (vlan *vlan) CreateVlanMember(ctx context.Context, r *saipb.CreateVlanMembe vlanAttrResp.GetAttr().MemberList = append(vlanAttrResp.GetAttr().MemberList, mOid) vlan.mgr.StoreAttributes(vOid, vlanAttrResp.GetAttr()) vlan.mu.Lock() - vlan.vlans[vOid][mOid] = &vlanMember{Oid: mOid, PortID: portID, Vid: vId, Mode: r.GetVlanTaggingMode()} + vlan.vlans[vOid][mOid] = &vlanMember{ + Oid: mOid, + PortID: portID, + Vid: vId, + Mode: r.GetVlanTaggingMode(), + BridgePortID: r.GetBridgePortId(), + } vlan.mu.Unlock() // Fetch the original vlan from the old vlan member and remove the member from that vlan diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index b8778dfc..04c3e650 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -1322,30 +1322,16 @@ func (sw *saiSwitch) onBridgeLearn(mac []byte, portID string) { return } - // Find the Bridge Port OID that wraps this physical port. var bridgePortOID uint64 - bpOIDs := sw.mgr.GetOIDsByType(saipb.ObjectType_OBJECT_TYPE_BRIDGE_PORT) - for _, bpOID := range bpOIDs { - val := sw.mgr.GetAttribute(strconv.FormatUint(bpOID, 10), int32(saipb.BridgePortAttr_BRIDGE_PORT_ATTR_PORT_ID)) - if val != nil { - if pID, ok := val.(uint64); ok && pID == portNum { - bridgePortOID = bpOID - break - } - } - } - - if bridgePortOID == 0 { - slog.Warn("onBridgeLearn: failed to find bridge port for physical port", "portNum", portNum) - return - } - - // Find the VLAN ID and VLAN OID for this port. - vlanID := uint32(DefaultVlanId) var vlanOID uint64 + var vlanID uint32 = uint32(DefaultVlanId) + if sw.vlan != nil { if member := sw.vlan.memberByPortId(portNum); member != nil { vlanID = member.Vid + if member.BridgePortID != 0 { + bridgePortOID = member.BridgePortID + } if oid, ok := sw.vlan.oidByVid(vlanID); ok { vlanOID = oid slog.Info("onBridgeLearn: found VLAN for port", "portNum", portNum, "vlanID", vlanID, "vlanOID", vlanOID) @@ -1359,6 +1345,28 @@ func (sw *saiSwitch) onBridgeLearn(mac []byte, portID string) { slog.Warn("onBridgeLearn: vlan server is nil, using default vlan") } + if bridgePortOID == 0 { + bpOIDs := sw.mgr.GetOIDsByType(saipb.ObjectType_OBJECT_TYPE_BRIDGE_PORT) + for _, bpOID := range bpOIDs { + req := &saipb.GetBridgePortAttributeRequest{ + Oid: bpOID, + AttrType: []saipb.BridgePortAttr{saipb.BridgePortAttr_BRIDGE_PORT_ATTR_PORT_ID}, + } + resp := &saipb.GetBridgePortAttributeResponse{} + if err := sw.mgr.PopulateAttributes(req, resp); err == nil && resp.GetAttr() != nil && resp.GetAttr().PortId != nil { + if resp.GetAttr().GetPortId() == portNum { + bridgePortOID = bpOID + break + } + } + } + } + + if bridgePortOID == 0 { + slog.Warn("onBridgeLearn: failed to find bridge port for physical port", "portNum", portNum) + return + } + swIDStr, ok := sw.mgr.GetSwitchID() if !ok { slog.Warn("onBridgeLearn: failed to get switch ID") diff --git a/dataplane/standalone/BUILD b/dataplane/standalone/BUILD index 272b5534..7b432799 100644 --- a/dataplane/standalone/BUILD +++ b/dataplane/standalone/BUILD @@ -18,6 +18,10 @@ cc_library( cc_binary( name = "sai", + linkopts = [ + "-static-libstdc++", + "-static-libgcc", + ], linkshared = True, linkstatic = True, deps = [ diff --git a/dataplane/standalone/sai/fdb.cc b/dataplane/standalone/sai/fdb.cc index a993686a..eed269f0 100644 --- a/dataplane/standalone/sai/fdb.cc +++ b/dataplane/standalone/sai/fdb.cc @@ -99,6 +99,9 @@ sai_status_t l_create_fdb_entry(const sai_fdb_entry_t* fdb_entry, lemming::dataplane::sai::CreateFdbEntryRequest req = convert_create_fdb_entry(attr_count, attr_list); + if (fdb_entry != nullptr) { + *req.mutable_entry() = convert_from_fdb_entry(*fdb_entry); + } lemming::dataplane::sai::CreateFdbEntryResponse resp; grpc::ClientContext context; @@ -121,6 +124,9 @@ sai_status_t l_remove_fdb_entry(const sai_fdb_entry_t* fdb_entry) { LOG(INFO) << "Func: " << __PRETTY_FUNCTION__; lemming::dataplane::sai::RemoveFdbEntryRequest req; + if (fdb_entry != nullptr) { + *req.mutable_entry() = convert_from_fdb_entry(*fdb_entry); + } lemming::dataplane::sai::RemoveFdbEntryResponse resp; grpc::ClientContext context; @@ -144,6 +150,9 @@ sai_status_t l_set_fdb_entry_attribute(const sai_fdb_entry_t* fdb_entry, LOG(INFO) << "Func: " << __PRETTY_FUNCTION__; lemming::dataplane::sai::SetFdbEntryAttributeRequest req; + if (fdb_entry != nullptr) { + *req.mutable_entry() = convert_from_fdb_entry(*fdb_entry); + } lemming::dataplane::sai::SetFdbEntryAttributeResponse resp; grpc::ClientContext context; @@ -196,6 +205,9 @@ sai_status_t l_get_fdb_entry_attribute(const sai_fdb_entry_t* fdb_entry, LOG(INFO) << "Func: " << __PRETTY_FUNCTION__; lemming::dataplane::sai::GetFdbEntryAttributeRequest req; + if (fdb_entry != nullptr) { + *req.mutable_entry() = convert_from_fdb_entry(*fdb_entry); + } lemming::dataplane::sai::GetFdbEntryAttributeResponse resp; grpc::ClientContext context; @@ -284,6 +296,9 @@ sai_status_t l_create_fdb_entries(uint32_t object_count, for (uint32_t i = 0; i < object_count; i++) { auto r = convert_create_fdb_entry(attr_count[i], attr_list[i]); + if (fdb_entry != nullptr) { + *r.mutable_entry() = convert_from_fdb_entry(fdb_entry[i]); + } *req.add_reqs() = r; } @@ -319,6 +334,11 @@ sai_status_t l_remove_fdb_entries(uint32_t object_count, grpc::ClientContext context; for (uint32_t i = 0; i < object_count; i++) { + lemming::dataplane::sai::RemoveFdbEntryRequest r; + if (fdb_entry != nullptr) { + *r.mutable_entry() = convert_from_fdb_entry(fdb_entry[i]); + } + *req.add_reqs() = r; } grpc::Status status = fdb->RemoveFdbEntries(&context, req, &resp);