diff --git a/dataplane/BUILD b/dataplane/BUILD index b1f95896..6768a5b8 100644 --- a/dataplane/BUILD +++ b/dataplane/BUILD @@ -18,6 +18,7 @@ go_library( "//gnmi/oc", "//gnmi/reconciler", "//proto/forwarding", + "@com_github_golang_glog//:glog", "@com_github_openconfig_gnmi//proto/gnmi", "@com_github_openconfig_ygnmi//ygnmi", "@org_golang_google_grpc//:go_default_library", diff --git a/dataplane/saiserver/acl.go b/dataplane/saiserver/acl.go index 0ba5e724..3a78d5aa 100644 --- a/dataplane/saiserver/acl.go +++ b/dataplane/saiserver/acl.go @@ -387,10 +387,25 @@ func (m *myMac) CreateMyMac(ctx context.Context, req *saipb.CreateMyMacRequest) EntryDesc: ed, Actions: []*fwdpb.ActionDesc{{ActionType: fwdpb.ActionType_ACTION_TYPE_CONTINUE}}, } - if _, err := m.dataplane.TableEntryAdd(ctx, mReq); err != nil { return nil, err } + m.myMacTable[id] = mi + // Populate the switch attribute. + saReq := &saipb.GetSwitchAttributeRequest{ + Oid: switchID, + AttrType: []saipb.SwitchAttr{ + saipb.SwitchAttr_SWITCH_ATTR_MY_MAC_LIST, + }, + } + saResp := &saipb.GetSwitchAttributeResponse{} + if err := m.mgr.PopulateAttributes(saReq, saResp); err != nil { + return nil, fmt.Errorf("Failed to populate switch attributes: %v", err) + } + mml := append(saResp.GetAttr().MyMacList, id) + m.mgr.StoreAttributes(switchID, &saipb.SwitchAttribute{ + MyMacList: mml, + }) return &saipb.CreateMyMacResponse{Oid: id}, nil } diff --git a/integration_tests/dataplane/mymac/BUILD b/integration_tests/dataplane/mymac/BUILD new file mode 100644 index 00000000..4569b6ce --- /dev/null +++ b/integration_tests/dataplane/mymac/BUILD @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "mymac_test", + srcs = ["mymac_test.go"], + data = [":testbed.pb.txt"], + deps = [ + "//dataplane/proto/sai", + "//internal/attrs", + "//internal/binding", + "@com_github_open_traffic_generator_snappi_gosnappi//:gosnappi", + "@com_github_openconfig_ondatra//:ondatra", + "@com_github_openconfig_ondatra//binding", + "@com_github_openconfig_ondatra//gnmi", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_protobuf//proto", + ], +) diff --git a/integration_tests/dataplane/mymac/mymac_test.go b/integration_tests/dataplane/mymac/mymac_test.go new file mode 100644 index 00000000..f5bd85c5 --- /dev/null +++ b/integration_tests/dataplane/mymac/mymac_test.go @@ -0,0 +1,310 @@ +// 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 +// +// https://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 mymac + +import ( + "context" + "fmt" + "net" + "strconv" + "testing" + "time" + + "github.com/open-traffic-generator/snappi/gosnappi" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + + "github.com/openconfig/lemming/internal/attrs" + "github.com/openconfig/lemming/internal/binding" + + obind "github.com/openconfig/ondatra/binding" + + saipb "github.com/openconfig/lemming/dataplane/proto/sai" +) + +const ( + ipv4PrefixLen = 30 + ateDstNetCIDR = "198.51.100.0/24" + ateIndirectNH = "203.0.113.1" + ateIndirectNHCIDR = ateIndirectNH + "/32" + nhIndex = 1 + nhgIndex = 42 + nhIndex2 = 2 + nhgIndex2 = 52 + nhIndex3 = 3 + nhgIndex3 = 62 + defaultNetworkInstance = "DEFAULT" +) + +var ( + dutPort1 = attrs.Attributes{ + Desc: "dutPort1", + MAC: "10:10:10:10:10:10", + IPv4: "192.0.2.1", + IPv4Len: ipv4PrefixLen, + } + + atePort1 = attrs.Attributes{ + Name: "port1", + MAC: "02:00:01:01:01:01", + IPv4: "192.0.2.2", + IPv4Len: ipv4PrefixLen, + } + + dutPort2 = attrs.Attributes{ + Desc: "dutPort2", + MAC: "10:10:10:10:10:11", + IPv4: "192.0.2.5", + IPv4Len: ipv4PrefixLen, + } + + atePort2 = attrs.Attributes{ + Name: "port2", + MAC: "02:00:02:01:01:01", + IPv4: "192.0.2.6", + IPv4Len: ipv4PrefixLen, + } +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, binding.Local(".")) +} + +func dataplaneConn(t testing.TB, dut *ondatra.DUTDevice) *grpc.ClientConn { + t.Helper() + var lemming interface { + DataplaneConn(ctx context.Context, opts ...grpc.DialOption) (*grpc.ClientConn, error) + } + if err := obind.DUTAs(dut.RawAPIs().BindingDUT(), &lemming); err != nil { + t.Fatalf("failed to get lemming dut: %v", err) + } + conn, err := lemming.DataplaneConn(context.Background()) + if err != nil { + t.Fatal(err) + } + return conn +} + +func configureDUT(t testing.TB, dut *ondatra.DUTDevice) { + t.Helper() + conn := dataplaneConn(t, dut) + ric := saipb.NewRouterInterfaceClient(conn) + port1ID, err := strconv.ParseUint(dut.Port(t, "port1").Name(), 10, 64) + if err != nil { + t.Fatal(err) + } + port2ID, err := strconv.ParseUint(dut.Port(t, "port2").Name(), 10, 64) + if err != nil { + t.Fatal(err) + } + + mac1, err := net.ParseMAC(dutPort1.MAC) + if err != nil { + t.Fatal(err) + } + _, err = ric.CreateRouterInterface(context.Background(), &saipb.CreateRouterInterfaceRequest{ + Switch: 1, + PortId: proto.Uint64(port1ID), + Type: saipb.RouterInterfaceType_ROUTER_INTERFACE_TYPE_PORT.Enum(), + SrcMacAddress: mac1, + }) + if err != nil { + t.Fatal(err) + } + mac2, err := net.ParseMAC(dutPort2.MAC) + if err != nil { + t.Fatal(err) + } + rif2Resp, err := ric.CreateRouterInterface(context.Background(), &saipb.CreateRouterInterfaceRequest{ + Switch: 1, + PortId: proto.Uint64(port2ID), + Type: saipb.RouterInterfaceType_ROUTER_INTERFACE_TYPE_PORT.Enum(), + SrcMacAddress: mac2, + }) + if err != nil { + t.Fatal(err) + } + + rc := saipb.NewRouteClient(conn) + _, err = rc.CreateRouteEntry(context.Background(), &saipb.CreateRouteEntryRequest{ + Entry: &saipb.RouteEntry{ + SwitchId: 1, + VrId: 0, + Destination: &saipb.IpPrefix{ + Addr: []byte{192, 0, 2, 6}, + Mask: []byte{255, 255, 255, 255}, + }, + }, + PacketAction: saipb.PacketAction_PACKET_ACTION_FORWARD.Enum(), + NextHopId: &rif2Resp.Oid, + }) + if err != nil { + t.Fatal(err) + } + + nc := saipb.NewNeighborClient(conn) + _, err = nc.CreateNeighborEntry(context.Background(), &saipb.CreateNeighborEntryRequest{ + Entry: &saipb.NeighborEntry{ + SwitchId: 1, + RifId: rif2Resp.Oid, + IpAddress: []byte{192, 0, 2, 6}, + }, + DstMacAddress: []byte{0o2, 0o0, 0o2, 0o1, 0o1, 0o1}, + }) + if err != nil { + t.Fatal(err) + } +} + +// clearMyMac removes all entities in MyMac table. +func clearMyMac(t *testing.T, dut *ondatra.DUTDevice) error { + // Try to get the MyMac list, and there should be only one entry. + conn := dataplaneConn(t, dut) + mmc := saipb.NewSwitchClient(conn) + var err error + req := &saipb.GetSwitchAttributeRequest{ + Oid: 1, + AttrType: []saipb.SwitchAttr{saipb.SwitchAttr_SWITCH_ATTR_MY_MAC_LIST}, + } + resp, err := mmc.GetSwitchAttribute(context.Background(), req) + if err != nil { + return err + } + oids := resp.GetAttr().MyMacList + if oids == nil { + return nil + } + for _, oid := range oids { + mmc := saipb.NewMyMacClient(conn) + if _, err := mmc.RemoveMyMac(context.Background(), &saipb.RemoveMyMacRequest{ + Oid: oid, + }); err != nil { + return fmt.Errorf("Failed to remove MyMac: %+v", err) + } + } + t.Logf("MyMac table cleared.") + return nil +} + +// restoreMyMac restores the rule to allow all traffic to send to L3. +func restoreMyMac(t *testing.T, dut *ondatra.DUTDevice) error { + // Allow all traffic to L3 processing. + conn := dataplaneConn(t, dut) + mmc := saipb.NewMyMacClient(conn) + _, err := mmc.CreateMyMac(context.Background(), &saipb.CreateMyMacRequest{ + Switch: 1, + Priority: proto.Uint32(1), + MacAddress: []byte{0, 0, 0, 0, 0, 0}, + MacAddressMask: []byte{0, 0, 0, 0, 0, 0}, + }) + if err != nil { + return err + } + t.Logf("MyMac table restored.") + return nil +} + +func TestMyMac(t *testing.T) { + // TODO: add more sub-tests when the Magna issue is resolved. + tests := []struct { + desc string + clearMyMac bool + macAddr []byte + macAddrMask []byte + passed bool + }{{ + desc: "Traffic dropped", // Remove the default entry that allows all traffic to L3. + clearMyMac: true, + passed: false, + }} + ate := ondatra.ATE(t, "ate") + ateTop := configureATE(t, ate) + ate.OTG().PushConfig(t, ateTop) + ate.OTG().StartProtocols(t) + + dut := ondatra.DUT(t, "dut") + configureDUT(t, dut) + + for _, tt := range tests { + tx, rx := testTraffic(t, ate, ateTop, atePort1, dutPort1, atePort2, 10*time.Second, dut, tt.clearMyMac) + t.Logf("[%s] Got TX: %d, RX: %d", tt.desc, tx, rx) + if tx == 0 { + t.Fatalf("No packet sent") + } + switch { + case tt.passed && rx != tx, !tt.passed && rx != 0: + t.Errorf("got %d, expect %d", rx, tx) + } + } +} + +// configureATE configures port1 and port2 on the ATE. +func configureATE(t *testing.T, ate *ondatra.ATEDevice) gosnappi.Config { + top := gosnappi.NewConfig() + + p1 := ate.Port(t, "port1") + p2 := ate.Port(t, "port2") + + atePort1.AddToOTG(top, p1, &dutPort1) + atePort2.AddToOTG(top, p2, &dutPort2) + return top +} + +// testTraffic generates traffic flow from source network to +// destination network via srcEndPoint to dstEndPoint and checks for +// packet loss and returns the number of tx and rx packets. +func testTraffic(t *testing.T, ate *ondatra.ATEDevice, top gosnappi.Config, srcEndPoint, srcPeerEndpoint, dstEndPoint attrs.Attributes, dur time.Duration, dut *ondatra.DUTDevice, clearMyMacTable bool) (uint64, uint64) { + if clearMyMacTable { + clearMyMac(t, dut) + defer restoreMyMac(t, dut) + } + otg := ate.OTG() + top.Flows().Clear().Items() + + ipFLow := top.Flows().Add().SetName("Flow") + ipFLow.Metrics().SetEnable(true) + ipFLow.TxRx().Port().SetTxName(srcEndPoint.Name).SetRxNames([]string{dstEndPoint.Name}) + + ipFLow.Rate().SetPps(10) + + // OTG specifies that the order of the .Packet().Add() calls determines + // the stack of headers that are to be used, starting at the outer-most and + // ending with the inner-most. + + // Set up ethernet layer. + eth := ipFLow.Packet().Add().Ethernet() + eth.Src().SetValue(srcEndPoint.MAC) + eth.Dst().SetValue(srcPeerEndpoint.MAC) + + ip4 := ipFLow.Packet().Add().Ipv4() + ip4.Src().SetValue(srcEndPoint.IPv4) + ip4.Dst().SetValue(dstEndPoint.IPv4) + ip4.Version().SetValue(4) + + otg.PushConfig(t, top) + + otg.StartTraffic(t) + time.Sleep(dur) + t.Logf("Stop traffic") + otg.StopTraffic(t) + + time.Sleep(5 * time.Second) + + txPkts := gnmi.Get(t, otg, gnmi.OTG().Flow("Flow").Counters().OutPkts().State()) + rxPkts := gnmi.Get(t, otg, gnmi.OTG().Flow("Flow").Counters().InPkts().State()) + return txPkts, rxPkts +} diff --git a/integration_tests/dataplane/mymac/testbed.pb.txt b/integration_tests/dataplane/mymac/testbed.pb.txt new file mode 100644 index 00000000..b67a0943 --- /dev/null +++ b/integration_tests/dataplane/mymac/testbed.pb.txt @@ -0,0 +1,36 @@ + +# proto-file: github.com/openconfig/ondatra/blob/main/proto/testbed.proto +# proto-message: ondatra.Testbed + +# This testbed provides 2 links between a DUT and an ATE connected +# pairwise. + +duts { + id: "dut" + ports { + id: "port1" + } + ports { + id: "port2" + } +} + +ates { + id: "ate" + ports { + id: "port1" + } + ports { + id: "port2" + } +} + +links { + a: "dut:port1" + b: "ate:port1" +} + +links { + a: "dut:port2" + b: "ate:port2" +}