diff --git a/dataplane/saiserver/BUILD b/dataplane/saiserver/BUILD index b15613c2..8b341192 100644 --- a/dataplane/saiserver/BUILD +++ b/dataplane/saiserver/BUILD @@ -46,6 +46,7 @@ go_test( "routing_test.go", "switch_test.go", "tunnel_test.go", + "vlan_test.go", ], embed = [":saiserver"], deps = [ diff --git a/dataplane/saiserver/attrmgr/attrmgr.go b/dataplane/saiserver/attrmgr/attrmgr.go index de8d1727..cf1a9c62 100644 --- a/dataplane/saiserver/attrmgr/attrmgr.go +++ b/dataplane/saiserver/attrmgr/attrmgr.go @@ -208,7 +208,7 @@ func (mgr *AttrMgr) PopulateAttributes(req, resp proto.Message) error { enumVal := reqList.Get(i).Enum() val, ok := mgr.attrs[id][int32(enumVal)] if !ok { - return fmt.Errorf("requested attribute not set: %v", reqList.Get(i)) + return fmt.Errorf("requested attribute not set: %v in OID: %v", reqList.Get(i), id) } // Empty lists exist so they are not errors, but are not settable. if val != nil { diff --git a/dataplane/saiserver/ports.go b/dataplane/saiserver/ports.go index 18c3a775..81629bc6 100644 --- a/dataplane/saiserver/ports.go +++ b/dataplane/saiserver/ports.go @@ -36,12 +36,13 @@ import ( fwdpb "github.com/openconfig/lemming/proto/forwarding" ) -func newPort(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server, opts *dplaneopts.Options) (*port, error) { +func newPort(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server, vlan saipb.VlanServer, opts *dplaneopts.Options) (*port, error) { p := &port{ mgr: mgr, dataplane: dataplane, portToEth: make(map[uint64]string), nextEth: 1, // Start at eth1 + vlan: vlan, opts: opts, } if opts.PortConfigFile != "" { @@ -61,6 +62,7 @@ func newPort(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server, type port struct { saipb.UnimplementedPortServer + vlan saipb.VlanServer mgr *attrmgr.AttrMgr dataplane switchDataplaneAPI nextEth int @@ -74,19 +76,21 @@ var getInterface = net.InterfaceByName func getForwardingPipeline() []*fwdpb.ActionDesc { return []*fwdpb.ActionDesc{ - fwdconfig.Action(fwdconfig.LookupAction(MyMacTable)).Build(), // Decide whether to process the packet. - 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. - fwdconfig.Action(fwdconfig.DecapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET)).Build(), // Decap L2 header. - fwdconfig.Action(fwdconfig.LookupAction(tunTermTable)).Build(), // Decap the packet if we have a tunnel. - fwdconfig.Action(fwdconfig.LookupAction(IngressActionTable)).Build(), // Run ingress action. - fwdconfig.Action(fwdconfig.LookupAction(FIBSelectorTable)).Build(), // Lookup in FIB. - fwdconfig.Action(fwdconfig.EncapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET)).Build(), // Encap L2 header. - fwdconfig.Action(fwdconfig.LookupAction(outputIfaceTable)).Build(), // Match interface to port - fwdconfig.Action(fwdconfig.LookupAction(NeighborTable)).Build(), // Lookup in the neighbor table. - fwdconfig.Action(fwdconfig.LookupAction(EgressActionTable)).Build(), // Run egress actions - fwdconfig.Action(fwdconfig.LookupAction(SRCMACTable)).Build(), // Lookup interface's MAC addr. + fwdconfig.Action(fwdconfig.LookupAction(VlanTable)).Build(), // Tag VLAN. + fwdconfig.Action(fwdconfig.LookupAction(MyMacTable)).Build(), // Decide whether to process the packet. + 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. + fwdconfig.Action(fwdconfig.DecapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET)).Build(), // Decap L2 header. + fwdconfig.Action(fwdconfig.LookupAction(tunTermTable)).Build(), // Decap the packet if we have a tunnel. + fwdconfig.Action(fwdconfig.LookupAction(IngressActionTable)).Build(), // Run ingress action. + fwdconfig.Action(fwdconfig.LookupAction(FIBSelectorTable)).Build(), // Lookup in FIB. + fwdconfig.Action(fwdconfig.EncapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET)).Build(), // Encap L2 header. + fwdconfig.Action(fwdconfig.LookupAction(outputIfaceTable)).Build(), // Match interface to port + fwdconfig.Action(fwdconfig.LookupAction(NeighborTable)).Build(), // Lookup in the neighbor table. + fwdconfig.Action(fwdconfig.LookupAction(EgressActionTable)).Build(), // Run egress actions + fwdconfig.Action(fwdconfig.LookupAction(SRCMACTable)).Build(), // Lookup interface's MAC addr. + fwdconfig.Action(fwdconfig.DecapAction(fwdpb.PacketHeaderId_PACKET_HEADER_ID_ETHERNET_VLAN)).Build(), // TODO: Revise the code if trunk mode needs to be supported. { ActionType: fwdpb.ActionType_ACTION_TYPE_OUTPUT, }, @@ -301,7 +305,21 @@ func (port *port) CreatePort(ctx context.Context, req *saipb.CreatePortRequest) } port.mgr.StoreAttributes(id, attrs) - + // Add the port to the default VLAN. + swAttr := &saipb.GetSwitchAttributeResponse{} + if err := port.mgr.PopulateAttributes(&saipb.GetSwitchAttributeRequest{ + Oid: 1, + AttrType: []saipb.SwitchAttr{saipb.SwitchAttr_SWITCH_ATTR_DEFAULT_VLAN_ID}, + }, swAttr); err != nil { + return nil, fmt.Errorf("Failed to retrive the default VLAN's OID. This is working as intended in unit tests.") + } + if _, err := port.vlan.CreateVlanMember(ctx, &saipb.CreateVlanMemberRequest{ + VlanId: proto.Uint64(swAttr.GetAttr().GetDefaultVlanId()), + BridgePortId: proto.Uint64(id), + VlanTaggingMode: saipb.VlanTaggingMode_VLAN_TAGGING_MODE_UNTAGGED.Enum(), + }); err != nil { + return nil, fmt.Errorf("Failed to add port to the default VLAN: %v", err) + } return &saipb.CreatePortResponse{ Oid: id, }, nil diff --git a/dataplane/saiserver/ports_test.go b/dataplane/saiserver/ports_test.go index 03da00cb..a9bc1f0a 100644 --- a/dataplane/saiserver/ports_test.go +++ b/dataplane/saiserver/ports_test.go @@ -45,7 +45,7 @@ func TestCreatePort(t *testing.T) { req: &saipb.CreatePortRequest{}, getInterfaceErr: fmt.Errorf("no interface"), want: &saipb.CreatePortResponse{ - Oid: 1, + Oid: 3, }, wantAttr: &saipb.PortAttribute{ OperStatus: saipb.PortOperStatus_PORT_OPER_STATUS_NOT_PRESENT.Enum(), @@ -107,7 +107,7 @@ func TestCreatePort(t *testing.T) { desc: "existing interface", req: &saipb.CreatePortRequest{}, want: &saipb.CreatePortResponse{ - Oid: 1, + Oid: 3, }, wantAttr: &saipb.PortAttribute{ OperStatus: saipb.PortOperStatus_PORT_OPER_STATUS_DOWN.Enum(), @@ -166,6 +166,7 @@ func TestCreatePort(t *testing.T) { Mtu: proto.Uint32(1514), }, }} + for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { getInterface = func(string) (*net.Interface, error) { @@ -185,7 +186,7 @@ func TestCreatePort(t *testing.T) { t.Errorf("CreatePort() failed: diff(-got,+want)\n:%s", d) } attr := &saipb.PortAttribute{} - if err := mgr.PopulateAllAttributes("1", attr); err != nil { + if err := mgr.PopulateAllAttributes("3", attr); err != nil { t.Fatal(err) } if d := cmp.Diff(attr, tt.wantAttr, protocmp.Transform()); d != "" { @@ -209,7 +210,7 @@ func TestCreatePorts(t *testing.T) { Reqs: []*saipb.CreatePortRequest{{}}, }, want: &saipb.CreatePortsResponse{ - Resps: []*saipb.CreatePortResponse{{Oid: 1}}, + Resps: []*saipb.CreatePortResponse{{Oid: 3}}, }, wantAttr: &saipb.PortAttribute{ OperStatus: saipb.PortOperStatus_PORT_OPER_STATUS_DOWN.Enum(), @@ -284,7 +285,7 @@ func TestCreatePorts(t *testing.T) { t.Errorf("CreatePorts() failed: diff(-got,+want)\n:%s", d) } attr := &saipb.PortAttribute{} - if err := mgr.PopulateAllAttributes("1", attr); err != nil { + if err := mgr.PopulateAllAttributes("3", attr); err != nil { t.Fatal(err) } if d := cmp.Diff(attr, tt.wantAttr, protocmp.Transform()); d != "" { @@ -305,11 +306,11 @@ func TestSetPortAttribute(t *testing.T) { }{{ desc: "admin status", req: &saipb.SetPortAttributeRequest{ - Oid: 1, + Oid: 3, AdminState: proto.Bool(false), }, wantReq: &fwdpb.PortStateRequest{ - PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: "1"}}, + PortId: &fwdpb.PortId{ObjectId: &fwdpb.ObjectId{Id: "3"}}, Operation: &fwdpb.PortInfo{AdminStatus: fwdpb.PortState_PORT_STATE_DISABLED_DOWN}, ContextId: &fwdpb.ContextId{Id: "foo"}, }, @@ -325,7 +326,7 @@ func TestSetPortAttribute(t *testing.T) { } dplane := &fakeSwitchDataplane{} c, mgr, stopFn := newTestPort(t, dplane) - mgr.StoreAttributes(1, &saipb.PortAttribute{ + mgr.StoreAttributes(3, &saipb.PortAttribute{ OperStatus: saipb.PortOperStatus_PORT_OPER_STATUS_DOWN.Enum(), }) defer stopFn() @@ -340,7 +341,7 @@ func TestSetPortAttribute(t *testing.T) { t.Errorf("SetPortAttribute() failed: diff(-got,+want)\n:%s", d) } attr := &saipb.PortAttribute{} - if err := mgr.PopulateAllAttributes("1", attr); err != nil { + if err := mgr.PopulateAllAttributes("3", attr); err != nil { t.Fatal(err) } if d := cmp.Diff(attr, tt.wantAttr, protocmp.Transform()); d != "" { @@ -360,7 +361,7 @@ func TestGetPortStats(t *testing.T) { }{{ desc: "all stats", req: &saipb.GetPortStatsRequest{ - Oid: 1, + Oid: 3, CounterIds: []saipb.PortStat{ saipb.PortStat_PORT_STAT_IF_IN_UCAST_PKTS, saipb.PortStat_PORT_STAT_IF_IN_NON_UCAST_PKTS, @@ -472,7 +473,25 @@ func TestRemovePort(t *testing.T) { func newTestPort(t testing.TB, api switchDataplaneAPI) (saipb.PortClient, *attrmgr.AttrMgr, func()) { conn, mgr, stopFn := newTestServer(t, func(mgr *attrmgr.AttrMgr, srv *grpc.Server) { - newPort(mgr, api, srv, &dplaneopts.Options{PortType: fwdpb.PortType_PORT_TYPE_KERNEL}) + // The following initialization code assigns OID 1 to the switch, and OID 2 to the VLAN. + // Therefore, the next available ID is 3. + vlan := newVlan(mgr, api, srv) + swID := mgr.NextID() + mgr.StoreAttributes(swID, &saipb.SwitchAttribute{ + DefaultStpInstId: proto.Uint64(101), + }) + resp, err := vlan.CreateVlan(context.Background(), &saipb.CreateVlanRequest{ + Switch: swID, + VlanId: proto.Uint32(DefaultVlanId), + LearnDisable: proto.Bool(true), + }) + if err != nil { + t.Fatalf("Failed to create a VLAN: %v", err) + } + mgr.StoreAttributes(swID, &saipb.SwitchAttribute{ + DefaultVlanId: proto.Uint64(resp.GetOid()), + }) + newPort(mgr, api, srv, vlan, &dplaneopts.Options{PortType: fwdpb.PortType_PORT_TYPE_KERNEL}) }) return saipb.NewPortClient(conn), mgr, stopFn } diff --git a/dataplane/saiserver/routing.go b/dataplane/saiserver/routing.go index 84563791..d44ee0b8 100644 --- a/dataplane/saiserver/routing.go +++ b/dataplane/saiserver/routing.go @@ -18,6 +18,7 @@ import ( "context" "encoding/binary" "fmt" + "sync" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -753,24 +754,70 @@ func (ri *routerInterface) GetRouterInterfaceStats(ctx context.Context, req *sai return &saipb.GetRouterInterfaceStatsResponse{Values: vals}, nil } +// vlanMember contains the info of a VLAN member. +type vlanMember struct { + Oid uint64 + PortID uint64 + Vid uint32 + Mode saipb.VlanTaggingMode +} + type vlan struct { + mu sync.Mutex saipb.UnimplementedVlanServer mgr *attrmgr.AttrMgr dataplane switchDataplaneAPI + oidByVId map[uint32]uint64 // VID -> VLAN_OID + vlans map[uint64]map[uint64]*vlanMember // VLAN_OID -> VLAN_Member_OID (port) } func newVlan(mgr *attrmgr.AttrMgr, dataplane switchDataplaneAPI, s *grpc.Server) *vlan { v := &vlan{ mgr: mgr, dataplane: dataplane, + oidByVId: map[uint32]uint64{}, + vlans: map[uint64]map[uint64]*vlanMember{}, } saipb.RegisterVlanServer(s, v) return v } -func (vlan *vlan) CreateVlan(context.Context, *saipb.CreateVlanRequest) (*saipb.CreateVlanResponse, error) { - id := vlan.mgr.NextID() +func (vlan *vlan) vidByOid(oid uint64) (uint32, error) { + for v, o := range vlan.oidByVId { + if o == oid { + return v, nil + } + } + return 0, fmt.Errorf("cannot find VLAN with OID %d", oid) +} + +func (vlan *vlan) memberByOid(oid uint64) *vlanMember { + for _, v := range vlan.vlans { + for mOid, member := range v { + if mOid == oid { + return member + } + } + } + return nil +} + +func (vlan *vlan) memberByPortId(oid uint64) *vlanMember { + for _, v := range vlan.vlans { + for _, member := range v { + if member.PortID == oid { + return member + } + } + } + return nil +} +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()) + } + id := vlan.mgr.NextID() req := &saipb.GetSwitchAttributeRequest{Oid: 1, AttrType: []saipb.SwitchAttr{saipb.SwitchAttr_SWITCH_ATTR_DEFAULT_STP_INST_ID}} resp := &saipb.GetSwitchAttributeResponse{} @@ -781,6 +828,7 @@ func (vlan *vlan) CreateVlan(context.Context, *saipb.CreateVlanRequest) (*saipb. attrs := &saipb.VlanAttribute{ MemberList: []uint64{}, StpInstance: resp.Attr.DefaultStpInstId, + VlanId: r.VlanId, UnknownNonIpMcastOutputGroupId: proto.Uint64(0), UnknownIpv4McastOutputGroupId: proto.Uint64(0), UnknownIpv6McastOutputGroupId: proto.Uint64(0), @@ -793,11 +841,127 @@ func (vlan *vlan) CreateVlan(context.Context, *saipb.CreateVlanRequest) (*saipb. TamObject: []uint64{}, } vlan.mgr.StoreAttributes(id, attrs) + vlan.vlans[id] = map[uint64]*vlanMember{} + vlan.oidByVId[r.GetVlanId()] = id return &saipb.CreateVlanResponse{ Oid: id, }, nil } +func (vlan *vlan) RemoveVlan(ctx context.Context, r *saipb.RemoveVlanRequest) (*saipb.RemoveVlanResponse, error) { + vId, err := vlan.vidByOid(r.GetOid()) + if err != nil { + return nil, fmt.Errorf("cannot find VLAN ID for OID %d", r.GetOid()) + } + if vId == DefaultVlanId { + return nil, fmt.Errorf("cannot remove default VLAN") + } + for _, v := range vlan.vlans[r.GetOid()] { + _, err := attrmgr.InvokeAndSave(ctx, vlan.mgr, vlan.RemoveVlanMember, &saipb.RemoveVlanMemberRequest{ + Oid: v.Oid, + }) + if err != nil { + return nil, err + } + } + // Update the internal map. + vlan.mu.Lock() + delete(vlan.vlans, r.GetOid()) + vlan.mu.Unlock() + return &saipb.RemoveVlanResponse{}, nil +} + +func (vlan *vlan) CreateVlanMember(ctx context.Context, r *saipb.CreateVlanMemberRequest) (*saipb.CreateVlanMemberResponse, error) { + vOid := r.GetVlanId() + vId, err := vlan.vidByOid(vOid) + if err != nil { + return nil, err + } + member := vlan.memberByPortId(r.GetBridgePortId()) // Keep the vlan member if this port was assigned to any VLAN. + mOid := vlan.mgr.NextID() + nid, err := vlan.dataplane.ObjectNID(ctx, &fwdpb.ObjectNIDRequest{ + ContextId: &fwdpb.ContextId{Id: vlan.dataplane.ID()}, + ObjectId: &fwdpb.ObjectId{Id: fmt.Sprint(r.GetBridgePortId())}, + }) + if err != nil { + return nil, err + } + vlanReq := fwdconfig.TableEntryAddRequest(vlan.dataplane.ID(), VlanTable).AppendEntry( + 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(), + } + if _, err := vlan.dataplane.TableEntryAdd(ctx, vlanReq); err != nil { + return nil, err + } + // Update the attributes and intenal data. + vlanAttrReq := &saipb.GetVlanAttributeRequest{Oid: vOid, AttrType: []saipb.VlanAttr{saipb.VlanAttr_VLAN_ATTR_MEMBER_LIST}} + vlanAttrResp := &saipb.GetVlanAttributeResponse{} + if err := vlan.mgr.PopulateAttributes(vlanAttrReq, vlanAttrResp); err != nil { + return nil, err + } + 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: r.GetBridgePortId(), Vid: vId, Mode: r.GetVlanTaggingMode()} + vlan.mu.Unlock() + if member != nil { + preVlanOid := vlan.oidByVId[member.Vid] + vlanAttrReq = &saipb.GetVlanAttributeRequest{Oid: preVlanOid, AttrType: []saipb.VlanAttr{saipb.VlanAttr_VLAN_ATTR_MEMBER_LIST}} + vlanAttrResp = &saipb.GetVlanAttributeResponse{} + if err := vlan.mgr.PopulateAttributes(vlanAttrReq, vlanAttrResp); err != nil { + return nil, err + } + newMemList := []uint64{} + for _, i := range vlanAttrResp.Attr.GetMemberList() { + if i != member.Oid { + newMemList = append(newMemList, i) + } + } + vlanAttrResp.GetAttr().MemberList = newMemList + vlan.mgr.StoreAttributes(preVlanOid, vlanAttrResp.GetAttr()) + // Update internal map. + vlan.mu.Lock() + delete(vlan.vlans[preVlanOid], member.Oid) + vlan.mu.Unlock() + } + return &saipb.CreateVlanMemberResponse{Oid: mOid}, nil +} + +func (vlan *vlan) RemoveVlanMember(ctx context.Context, r *saipb.RemoveVlanMemberRequest) (*saipb.RemoveVlanMemberResponse, error) { + member := vlan.memberByOid(r.GetOid()) + if member == nil { + return nil, fmt.Errorf("cannot find member with OID %d", r.GetOid()) + } + defVOid, ok := vlan.oidByVId[DefaultVlanId] + if !ok { + return nil, fmt.Errorf("cannot find default VLAN.") + } + // Move the port back to the default VLAN. + _, err := attrmgr.InvokeAndSave(ctx, vlan.mgr, vlan.CreateVlanMember, &saipb.CreateVlanMemberRequest{ + VlanId: proto.Uint64(defVOid), + BridgePortId: proto.Uint64(member.PortID), + VlanTaggingMode: saipb.VlanTaggingMode_VLAN_TAGGING_MODE_UNTAGGED.Enum(), + }) + if err != nil { + return nil, err + } + return &saipb.RemoveVlanMemberResponse{}, nil +} + +func (vlan *vlan) CreateVlanMembers(ctx context.Context, r *saipb.CreateVlanMembersRequest) (*saipb.CreateVlanMembersResponse, error) { + resp := &saipb.CreateVlanMembersResponse{} + for _, req := range r.GetReqs() { + res, err := attrmgr.InvokeAndSave(ctx, vlan.mgr, vlan.CreateVlanMember, req) + if err != nil { + return nil, err + } + resp.Resps = append(resp.Resps, res) + } + return resp, nil +} + type bridge struct { saipb.UnimplementedBridgeServer mgr *attrmgr.AttrMgr diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index a8a6aafa..8c2c022c 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -101,10 +101,13 @@ const ( hostifToPortTable = "cpu-input" portToHostifTable = "cpu-output" tunTermTable = "tun-term" + VlanTable = "vlan" + DefaultVlanId = 4095 // An reserved VLAN ID used as the default VLAN ID for internal usage. ) func newSwitch(mgr *attrmgr.AttrMgr, engine switchDataplaneAPI, s *grpc.Server, opts *dplaneopts.Options) (*saiSwitch, error) { - port, err := newPort(mgr, engine, s, opts) + vlan := newVlan(mgr, engine, s) + port, err := newPort(mgr, engine, s, vlan, opts) if err != nil { return nil, err } @@ -113,7 +116,7 @@ func newSwitch(mgr *attrmgr.AttrMgr, engine switchDataplaneAPI, s *grpc.Server, acl: newACL(mgr, engine, s), policer: newPolicer(mgr, engine, s), port: port, - vlan: newVlan(mgr, engine, s), + vlan: vlan, stp: &stp{}, vr: &virtualRouter{}, bridge: newBridge(mgr, engine, s), @@ -231,6 +234,26 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ if _, err := sw.dataplane.TableCreate(ctx, portMAC); err != nil { return nil, err } + vlanReq := &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: VlanTable}}, + 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_PACKET_PORT_INPUT, + }, + }}, + }, + }, + }, + } + if _, err := sw.dataplane.TableCreate(ctx, vlanReq); err != nil { + return nil, err + } myMAC := &fwdpb.TableCreateRequest{ ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, Desc: &fwdpb.TableDesc{ @@ -578,12 +601,14 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ sw.mgr.StoreAttributes(swID, &saipb.SwitchAttribute{DefaultStpInstId: &stpResp.Oid}) vlanResp, err := attrmgr.InvokeAndSave(ctx, sw.mgr, sw.vlan.CreateVlan, &saipb.CreateVlanRequest{ - Switch: swID, + Switch: swID, + VlanId: proto.Uint32(DefaultVlanId), // Create the default VLAN. + LearnDisable: proto.Bool(true), // TODO: figure out what does this do? }) if err != nil { return nil, err } - + sw.mgr.StoreAttributes(swID, &saipb.SwitchAttribute{DefaultVlanId: &vlanResp.Oid}) vrResp, err := attrmgr.InvokeAndSave(ctx, sw.mgr, sw.vr.CreateVirtualRouter, &saipb.CreateVirtualRouterRequest{ Switch: swID, }) @@ -695,7 +720,6 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ NatZoneCounterObjectId: proto.Uint64(0), } sw.mgr.StoreAttributes(swID, attrs) - return &saipb.CreateSwitchResponse{ Oid: swID, }, nil diff --git a/dataplane/saiserver/vlan_test.go b/dataplane/saiserver/vlan_test.go new file mode 100644 index 00000000..a16945c8 --- /dev/null +++ b/dataplane/saiserver/vlan_test.go @@ -0,0 +1,273 @@ +// Copyright 2024 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 ( + "context" + "fmt" + "testing" + + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/google/go-cmp/cmp" + "github.com/openconfig/gnmi/errdiff" + + "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" + + saipb "github.com/openconfig/lemming/dataplane/proto/sai" +) + +const ( + testStpInstId = uint64(101) +) + +func TestCreateVlan(t *testing.T) { + tests := []struct { + desc string + hasExistingVlan bool + req *saipb.CreateVlanRequest + want *saipb.CreateVlanResponse + wantAttr *saipb.VlanAttribute + wantErr string + }{{ + desc: "success", + req: &saipb.CreateVlanRequest{ + Switch: 1, + VlanId: proto.Uint32(DefaultVlanId), + LearnDisable: proto.Bool(true), + }, + want: &saipb.CreateVlanResponse{ + Oid: 1, + }, + wantAttr: &saipb.VlanAttribute{ + StpInstance: proto.Uint64(testStpInstId), + VlanId: proto.Uint32(DefaultVlanId), + LearnDisable: proto.Bool(true), + UnknownNonIpMcastOutputGroupId: proto.Uint64(0), + UnknownIpv4McastOutputGroupId: proto.Uint64(0), + UnknownIpv6McastOutputGroupId: proto.Uint64(0), + UnknownLinklocalMcastOutputGroupId: proto.Uint64(0), + IngressAcl: proto.Uint64(0), + EgressAcl: proto.Uint64(0), + UnknownUnicastFloodGroup: proto.Uint64(0), + UnknownMulticastFloodGroup: proto.Uint64(0), + BroadcastFloodGroup: proto.Uint64(0), + }, + }, { + desc: "existing VLAN", + hasExistingVlan: true, + req: &saipb.CreateVlanRequest{ + Switch: 1, + VlanId: proto.Uint32(DefaultVlanId), + LearnDisable: proto.Bool(true), + }, + wantErr: "found existing VLAN", + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + dplane := &fakeSwitchDataplane{} + c, mgr, stopFn := newTestVlan(t, dplane) + defer stopFn() + mgr.StoreAttributes(1, &saipb.SwitchAttribute{ + DefaultStpInstId: proto.Uint64(testStpInstId), + }) + if tt.hasExistingVlan { + if _, err := c.CreateVlan(context.TODO(), &saipb.CreateVlanRequest{ + Switch: 1, + VlanId: proto.Uint32(DefaultVlanId), + LearnDisable: proto.Bool(true), + }); err != nil { + t.Fatalf("failed to create VLAN") + } + } + got, gotErr := c.CreateVlan(context.TODO(), tt.req) + if diff := errdiff.Check(gotErr, tt.wantErr); diff != "" { + t.Fatalf("CreateVlan() unexpected err: %s w/ error %v", diff, gotErr) + } + if gotErr != nil { + return + } + if d := cmp.Diff(got, tt.want, protocmp.Transform()); d != "" { + t.Errorf("CreateVlan() failed: diff(-got,+want)\n:%s", d) + } + attr := &saipb.VlanAttribute{} + if err := mgr.PopulateAllAttributes("1", attr); err != nil { + t.Fatal(err) + } + if d := cmp.Diff(attr, tt.wantAttr, protocmp.Transform()); d != "" { + t.Errorf("CreateVlan() failed: diff(-got,+want)\n:%s", d) + } + }) + } +} + +// TestVlanOperations is an end-to-end test. +func TestVlanOperations(t *testing.T) { + testPort1Id := uint64(11) + testPort2Id := uint64(12) + testPort3Id := uint64(13) + testPort4Id := uint64(14) + testVlanId := uint32(10) + dplane := &fakeSwitchDataplane{} + c, mgr, stopFn := newTestVlan(t, dplane) + defer stopFn() + mgr.StoreAttributes(1, &saipb.SwitchAttribute{ + DefaultStpInstId: proto.Uint64(testStpInstId), + }) + ctx := context.TODO() + + getVLANMembers := func(vlanOID uint64) ([]uint64, error) { + vlanAttrReq := &saipb.GetVlanAttributeRequest{Oid: vlanOID, AttrType: []saipb.VlanAttr{saipb.VlanAttr_VLAN_ATTR_MEMBER_LIST}} + vlanAttrResp := &saipb.GetVlanAttributeResponse{} + if err := mgr.PopulateAttributes(vlanAttrReq, vlanAttrResp); err != nil { + return nil, fmt.Errorf("failed to populate attributes of default VLAN.") + } + return vlanAttrResp.GetAttr().GetMemberList(), nil + } + + // By default, the default VLAN has all the ports (port1 to port4). + resp, err := c.CreateVlan(ctx, &saipb.CreateVlanRequest{ + Switch: 1, + VlanId: proto.Uint32(DefaultVlanId), + LearnDisable: proto.Bool(true), + }) + if err != nil { + t.Fatalf("Failed to create VLAN: %v", err) + } + vlanOID := resp.GetOid() + resps, err := c.CreateVlanMembers(ctx, &saipb.CreateVlanMembersRequest{ + Reqs: []*saipb.CreateVlanMemberRequest{ + { + Switch: 1, + VlanId: &vlanOID, + BridgePortId: &testPort1Id, + }, { + Switch: 1, + VlanId: &vlanOID, + BridgePortId: &testPort2Id, + }, { + Switch: 1, + VlanId: &vlanOID, + BridgePortId: &testPort3Id, + }, { + Switch: 1, + VlanId: &vlanOID, + BridgePortId: &testPort4Id, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to add ports to the default VLAN: %v", err) + } + defVLANMembers := []uint64{} + for _, r := range resps.GetResps() { + defVLANMembers = append(defVLANMembers, r.GetOid()) + } + // Ensure default VLAN's member list is correct. + members, err := getVLANMembers(vlanOID) + if err != nil { + t.Fatalf("Failed to populate attributes of the default VLAN: %v", err) + } + if diff := cmp.Diff(defVLANMembers, members); diff != "" { + t.Fatalf("Failed to create VLAN members: %v", diff) + } + + // Creates a non-default VLAN with two ports. These two ports are moved from + // the default VLAN to the new VLAN. + resp, err = c.CreateVlan(ctx, &saipb.CreateVlanRequest{ + Switch: 1, + VlanId: proto.Uint32(testVlanId), + LearnDisable: proto.Bool(true), + }) + if err != nil { + t.Fatalf("Failed to create a non-default VLAN %d: %v", testVlanId, err) + } + testVlanOID := resp.GetOid() + resps, err = c.CreateVlanMembers(ctx, &saipb.CreateVlanMembersRequest{ + Reqs: []*saipb.CreateVlanMemberRequest{ + { + Switch: 1, + VlanId: &testVlanOID, + BridgePortId: &testPort1Id, + }, { + Switch: 1, + VlanId: &testVlanOID, + BridgePortId: &testPort2Id, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to add ports to the non-default VLAN: %v", err) + } + testVLANMembers := []uint64{} + for _, r := range resps.GetResps() { + testVLANMembers = append(testVLANMembers, r.GetOid()) + } + testm, err := getVLANMembers(testVlanOID) + if err != nil { + t.Fatalf("Failed to get members of the non-default VLAN: %v", err) + } + defaultm, err := getVLANMembers(vlanOID) + if err != nil { + t.Fatalf("Failed to get members of the default VLAN: %v", err) + } + if len(testm) != 2 && len(defaultm) != 2 { + t.Fatalf("Failed to create a non-default VLAN") + } + + // Remove one port from the non-default VLAN, and ensure it is moved to the default VLAN. + _, err = c.RemoveVlanMember(ctx, &saipb.RemoveVlanMemberRequest{ + Oid: testVLANMembers[0], + }) + if err != nil { + t.Fatalf("Failed to remove VLAN member (OID=%d): %v", testVLANMembers[0], err) + } + testm, err = getVLANMembers(testVlanOID) + if err != nil { + t.Fatalf("Failed to get members of the non-default VLAN: %v", err) + } + defaultm, err = getVLANMembers(vlanOID) + if err != nil { + t.Fatalf("Failed to get members of default VLAN: %v", err) + } + if len(testm) != 1 && len(defaultm) != 3 { + t.Errorf("Failed to remove a non-default VLAN member") + } + + // Remove the non-default VLAN. + _, err = c.RemoveVlan(ctx, &saipb.RemoveVlanRequest{ + Oid: testVlanOID, + }) + if err != nil { + t.Fatalf("Failed to remove the non-default VLAN %d: %v", testVlanId, err) + } + defaultm, err = getVLANMembers(vlanOID) + if err != nil { + t.Fatalf("Failed to get members of the default VLAN: %v", err) + } + // Ensure all of the 4 ports are now under the default VLAN. + if len(defaultm) != 4 { + t.Errorf("Failed to remove the non-default VLAN") + } +} + +func newTestVlan(t testing.TB, api switchDataplaneAPI) (saipb.VlanClient, *attrmgr.AttrMgr, func()) { + conn, mgr, stopFn := newTestServer(t, func(mgr *attrmgr.AttrMgr, srv *grpc.Server) { + newVlan(mgr, api, srv) + }) + return saipb.NewVlanClient(conn), mgr, stopFn +} diff --git a/integration_tests/dataplane/basictraffic/basic_traffic_test.go b/integration_tests/dataplane/basictraffic/basic_traffic_test.go index 91779fd9..c0e5f496 100644 --- a/integration_tests/dataplane/basictraffic/basic_traffic_test.go +++ b/integration_tests/dataplane/basictraffic/basic_traffic_test.go @@ -119,7 +119,9 @@ func configureDUT(t testing.TB, dut *ondatra.DUTDevice) { MacAddress: []byte{0, 0, 0, 0, 0, 0}, MacAddressMask: []byte{0, 0, 0, 0, 0, 0}, }) - + if err != nil { + t.Fatal(err) + } mac1, err := net.ParseMAC(dutPort1.MAC) if err != nil { t.Fatal(err) @@ -146,7 +148,6 @@ func configureDUT(t testing.TB, dut *ondatra.DUTDevice) { if err != nil { t.Fatal(err) } - rc := saipb.NewRouteClient(conn) _, err = rc.CreateRouteEntry(context.Background(), &saipb.CreateRouteEntryRequest{ Entry: &saipb.RouteEntry{ diff --git a/integration_tests/dataplane/mymac/mymac_test.go b/integration_tests/dataplane/mymac/mymac_test.go index 467a7eb0..f8ae647a 100644 --- a/integration_tests/dataplane/mymac/mymac_test.go +++ b/integration_tests/dataplane/mymac/mymac_test.go @@ -137,7 +137,6 @@ func configureDUT(t testing.TB, conn *grpc.ClientConn, dut *ondatra.DUTDevice) { if err != nil { t.Fatal(err) } - rc := saipb.NewRouteClient(conn) _, err = rc.CreateRouteEntry(context.Background(), &saipb.CreateRouteEntryRequest{ Entry: &saipb.RouteEntry{