From 66f7d71c90751d348203cf068812a52add749ae7 Mon Sep 17 00:00:00 2001 From: Daniel Grau Date: Thu, 12 Dec 2024 22:06:54 +0000 Subject: [PATCH] Test runner + example for dataplane tests --- .../dataplane/mplsoudp/mplsoverudp_test.go | 2 +- integration_tests/dataplane/subintf/BUILD | 19 ++ .../dataplane/subintf/subintf_test.go | 132 ++++++++++++++ .../dataplane/subintf/testbed.pb.txt | 9 + integration_tests/saiutil/BUILD | 11 +- integration_tests/saiutil/runner.go | 109 ++++++++++++ integration_tests/saiutil/saiutil.go | 167 +++++++++++++++++- 7 files changed, 445 insertions(+), 4 deletions(-) create mode 100644 integration_tests/dataplane/subintf/BUILD create mode 100644 integration_tests/dataplane/subintf/subintf_test.go create mode 100644 integration_tests/dataplane/subintf/testbed.pb.txt create mode 100644 integration_tests/saiutil/runner.go diff --git a/integration_tests/dataplane/mplsoudp/mplsoverudp_test.go b/integration_tests/dataplane/mplsoudp/mplsoverudp_test.go index 7b0befcc..2285c117 100644 --- a/integration_tests/dataplane/mplsoudp/mplsoverudp_test.go +++ b/integration_tests/dataplane/mplsoudp/mplsoverudp_test.go @@ -163,7 +163,7 @@ func configureDUT(t testing.TB, dut *ondatra.DUTDevice, hop *oc.NetworkInstance_ if _, err := fwd.TableEntryAdd(context.Background(), actReq); err != nil { t.Fatal(err) } - saiutil.CreateRoute(t, dut, routePrefix, nh.GetOid()) + saiutil.CreateRoute(t, dut, routePrefix, nh.GetOid(), 0) saiutil.CreateNeighbor(t, dut, *hop.IpAddress, neighborMAC, outRIF) } diff --git a/integration_tests/dataplane/subintf/BUILD b/integration_tests/dataplane/subintf/BUILD new file mode 100644 index 00000000..ede1c127 --- /dev/null +++ b/integration_tests/dataplane/subintf/BUILD @@ -0,0 +1,19 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "subintf_test", + srcs = ["subintf_test.go"], + data = [ + "testbed.pb.txt", + ], + deps = [ + "//gnmi/oc", + "//integration_tests/saiutil", + "//internal/attrs", + "//internal/binding", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", + "@com_github_openconfig_ondatra//:ondatra", + "@org_golang_google_protobuf//proto", + ], +) diff --git a/integration_tests/dataplane/subintf/subintf_test.go b/integration_tests/dataplane/subintf/subintf_test.go new file mode 100644 index 00000000..81050b34 --- /dev/null +++ b/integration_tests/dataplane/subintf/subintf_test.go @@ -0,0 +1,132 @@ +// 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 subintf + +import ( + "net" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ondatra" + "google.golang.org/protobuf/proto" + + "github.com/openconfig/lemming/gnmi/oc" + "github.com/openconfig/lemming/integration_tests/saiutil" + "github.com/openconfig/lemming/internal/attrs" + "github.com/openconfig/lemming/internal/binding" +) + +var pm = &binding.PortMgr{} + +func TestMain(m *testing.M) { + ondatra.RunTests(m, binding.Local(".", binding.WithOverridePortManager(pm))) +} + +var ( + dutPort1 = attrs.Attributes{ + Desc: "dutPort1", + MAC: "10:10:10:10:10:10", + } + + dutPort2 = attrs.Attributes{ + Desc: "dutPort2", + MAC: "10:10:10:10:10:11", + } +) + +func TestVLANSubIntfMatch(t *testing.T) { + s := saiutil.NewSuite() + s.BaseConfig = []saiutil.ConfigOp{ + saiutil.ConfigVRF("DEFAULT"), + saiutil.ConfigVRF("NON_DEFAULT"), + saiutil.ConfigRIF("port1", dutPort1.MAC, "DEFAULT"), + saiutil.ConfigRIF("port2", dutPort2.MAC, "DEFAULT"), + saiutil.ConfigVLANSubIntf("port1", 1, "10:10:10:10:10:12", 100, "NON_DEFAULT"), + } + s.Case = []*saiutil.Case{{ + Config: []saiutil.ConfigOp{ + saiutil.ConfigAft("NON_DEFAULT", &oc.NetworkInstance_Afts{ + Ipv4Entry: map[string]*oc.NetworkInstance_Afts_Ipv4Entry{ + "192.0.1.1/32": { + NextHopGroup: proto.Uint64(1), + }, + }, + NextHopGroup: map[uint64]*oc.NetworkInstance_Afts_NextHopGroup{ + 1: { + NextHop: map[uint64]*oc.NetworkInstance_Afts_NextHopGroup_NextHop{ + 1: {Weight: proto.Uint64(1), Index: proto.Uint64(1)}, + }, + }, + }, + NextHop: map[uint64]*oc.NetworkInstance_Afts_NextHop{ + 1: { + IpAddress: proto.String("192.0.2.2"), + InterfaceRef: &oc.NetworkInstance_Afts_NextHop_InterfaceRef{ + Interface: proto.String("port2"), + Subinterface: proto.Uint32(0), + }, + }, + }, + }), + saiutil.ConfigNeighbor(saiutil.InterfaceRef{Intf: "port2"}, "192.0.2.2", dutPort2.MAC), + }, + In: &saiutil.Packet{ + Port: "port1", + Layers: []gopacket.SerializableLayer{ + &layers.Ethernet{ + SrcMAC: saiutil.MustParseMac(t, "10:10:10:10:10:09"), + DstMAC: saiutil.MustParseMac(t, "10:10:10:10:10:10"), + EthernetType: layers.EthernetTypeDot1Q, + }, + &layers.Dot1Q{ + Type: layers.EthernetTypeIPv4, + VLANIdentifier: 100, + }, + &layers.IPv4{ + SrcIP: net.ParseIP("192.0.2.1"), + DstIP: net.ParseIP("192.0.1.1"), + TTL: 10, + Version: 4, + Protocol: layers.IPProtocolNoNextHeader, + }, + gopacket.Payload{}, + }, + }, + Out: &saiutil.Packet{ + Port: "port2", + Layers: []gopacket.SerializableLayer{ + &layers.Ethernet{ + SrcMAC: saiutil.MustParseMac(t, "10:10:10:10:10:11"), + DstMAC: saiutil.MustParseMac(t, "10:10:10:10:10:11"), + EthernetType: layers.EthernetTypeIPv4, + }, + &layers.IPv4{ + SrcIP: net.ParseIP("192.0.2.1"), + DstIP: net.ParseIP("192.0.1.1"), + TTL: 9, + Version: 4, + Protocol: layers.IPProtocolNoNextHeader, + Length: 42, + Checksum: 11927, + IHL: 5, + }, + gopacket.Payload{}, + }, + }, + }} + + s.Run(t, pm) +} diff --git a/integration_tests/dataplane/subintf/testbed.pb.txt b/integration_tests/dataplane/subintf/testbed.pb.txt new file mode 100644 index 00000000..b3fbc9b7 --- /dev/null +++ b/integration_tests/dataplane/subintf/testbed.pb.txt @@ -0,0 +1,9 @@ +duts { + id: "dut" + ports { + id: "port1" + } + ports { + id: "port2" + } +} diff --git a/integration_tests/saiutil/BUILD b/integration_tests/saiutil/BUILD index b0158d57..3f3387ab 100644 --- a/integration_tests/saiutil/BUILD +++ b/integration_tests/saiutil/BUILD @@ -2,11 +2,20 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "saiutil", - srcs = ["saiutil.go"], + srcs = [ + "runner.go", + "saiutil.go", + ], importpath = "github.com/openconfig/lemming/integration_tests/saiutil", visibility = ["//visibility:public"], deps = [ "//dataplane/proto/sai", + "//gnmi/oc", + "//internal/binding", + "@com_github_google_go_cmp//cmp", + "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", "@com_github_openconfig_ondatra//:ondatra", "@com_github_openconfig_ondatra//binding", "@org_golang_google_grpc//:grpc", diff --git a/integration_tests/saiutil/runner.go b/integration_tests/saiutil/runner.go new file mode 100644 index 00000000..20f87f44 --- /dev/null +++ b/integration_tests/saiutil/runner.go @@ -0,0 +1,109 @@ +// 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 saiutil + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ondatra" + + "github.com/openconfig/lemming/internal/binding" +) + +// ConfigOp is interface for applying a dataplane configuration option. +type ConfigOp interface { + Config(t testing.TB, s *Suite, dut *ondatra.DUTDevice) + UnConfig(t testing.TB, s *Suite, dut *ondatra.DUTDevice) +} + +// NewSuite returns a empty test suite. +func NewSuite() *Suite { + return &Suite{ + interfaceMap: map[InterfaceRef]uint64{}, + oc2SAINextHop: map[uint64]uint64{}, + oc2SAINextHopGrp: map[uint64]uint64{}, + oc2SAIVRF: map[string]uint64{}, + } +} + +// Suite contains a baseconfig and sets of tests dataplane packet tests. +type Suite struct { + BaseConfig []ConfigOp + Case []*Case + + interfaceMap map[InterfaceRef]uint64 + oc2SAINextHop map[uint64]uint64 + oc2SAINextHopGrp map[uint64]uint64 + oc2SAIVRF map[string]uint64 +} + +// Packet is a list of layers and a corresponding port. +type Packet struct { + Layers []gopacket.SerializableLayer + Port string +} + +// A Case is single test case containing the config and the input and expect output. +type Case struct { + In, Out *Packet + Config []ConfigOp +} + +// Run runs all the cases and reports any diffs. +func (cases *Suite) Run(t testing.TB, pm *binding.PortMgr) { + dut := ondatra.DUT(t, "dut") + for _, cfg := range cases.BaseConfig { + cfg.Config(t, cases, dut) + } + for i, cs := range cases.Case { + t.Log("Running case ", i) + for _, cfg := range cs.Config { + cfg.Config(t, cases, dut) + } + buf := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true}, cs.In.Layers...); err != nil { + t.Fatalf("failed to serialize headers: %v", err) + } + p1 := pm.GetPort(dut.Port(t, cs.In.Port)) + p1.RXQueue.Write(buf.Bytes()) + p2 := pm.GetPort(dut.Port(t, cs.Out.Port)) + + select { + case packet := (<-p2.TXQueue.Receive()): + p := gopacket.NewPacket(packet.([]byte), layers.LayerTypeEthernet, gopacket.Default) + t.Logf("Got packet:\n%s", p.Dump()) + + got := []gopacket.SerializableLayer{} + for _, l := range p.Layers() { + got = append(got, l.(gopacket.SerializableLayer)) + } + // Skip the payload when comparing layers. + if d := cmp.Diff(got[0:len(got)-1], cs.Out.Layers[0:len(cs.Out.Layers)-1], cmpopts.IgnoreUnexported(layers.IPv6{}, layers.IPv4{}, layers.UDP{}, layers.MPLS{}), + cmpopts.IgnoreFields(layers.IPv4{}, "BaseLayer"), cmpopts.IgnoreFields(layers.UDP{}, "BaseLayer"), cmpopts.IgnoreFields(layers.Ethernet{}, "BaseLayer"), cmpopts.IgnoreFields(layers.IPv6{}, "BaseLayer")); d != "" { + t.Error(d) + } + case <-time.After(10 * time.Millisecond): + } + + for i := len(cs.Config) - 1; i >= 0; i-- { + cs.Config[i].UnConfig(t, cases, dut) + } + } +} diff --git a/integration_tests/saiutil/saiutil.go b/integration_tests/saiutil/saiutil.go index d9fe15b2..d3a41df4 100644 --- a/integration_tests/saiutil/saiutil.go +++ b/integration_tests/saiutil/saiutil.go @@ -1,3 +1,17 @@ +// 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 saiutil import ( @@ -13,6 +27,7 @@ import ( "google.golang.org/protobuf/proto" saipb "github.com/openconfig/lemming/dataplane/proto/sai" + "github.com/openconfig/lemming/gnmi/oc" ) func dataplaneConn(t testing.TB, dut *ondatra.DUTDevice) *grpc.ClientConn { @@ -30,6 +45,19 @@ func dataplaneConn(t testing.TB, dut *ondatra.DUTDevice) *grpc.ClientConn { return conn } +type configer struct { + cfg func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) + uncfg func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) +} + +func (c *configer) Config(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + c.cfg(t, s, dut) +} + +func (c *configer) UnConfig(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + c.uncfg(t, s, dut) +} + func CreateRIF(t testing.TB, dut *ondatra.DUTDevice, port *ondatra.Port, smac string) uint64 { t.Helper() conn := dataplaneConn(t, dut) @@ -54,7 +82,7 @@ func CreateRIF(t testing.TB, dut *ondatra.DUTDevice, port *ondatra.Port, smac st return resp.Oid } -func CreateRoute(t testing.TB, dut *ondatra.DUTDevice, prefix string, nexthop uint64) { +func CreateRoute(t testing.TB, dut *ondatra.DUTDevice, prefix string, nexthop uint64, vrId uint64) { t.Helper() conn := dataplaneConn(t, dut) rc := saipb.NewRouteClient(conn) @@ -68,7 +96,7 @@ func CreateRoute(t testing.TB, dut *ondatra.DUTDevice, prefix string, nexthop ui _, err = rc.CreateRouteEntry(context.Background(), &saipb.CreateRouteEntryRequest{ Entry: &saipb.RouteEntry{ SwitchId: 1, - VrId: 0, + VrId: vrId, Destination: &saipb.IpPrefix{ Addr: ip, Mask: mask, @@ -103,3 +131,138 @@ func CreateNeighbor(t testing.TB, dut *ondatra.DUTDevice, ip string, dmac string t.Fatal(err) } } + +type InterfaceRef struct { + Intf string + SubIntf uint32 +} + +func ConfigAft(vrf string, aft *oc.NetworkInstance_Afts) *configer { + c := &configer{} + + c.cfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + conn := dataplaneConn(t, dut) + nhc := saipb.NewNextHopClient(conn) + nhgc := saipb.NewNextHopGroupClient(conn) + + for id, nh := range aft.NextHop { + ref := InterfaceRef{ + Intf: nh.GetInterfaceRef().GetInterface(), + SubIntf: nh.GetInterfaceRef().GetSubinterface(), + } + req := &saipb.CreateNextHopRequest{ + Switch: 1, + Type: saipb.NextHopType_NEXT_HOP_TYPE_IP.Enum(), + RouterInterfaceId: proto.Uint64(s.interfaceMap[ref]), + Ip: net.ParseIP(*nh.IpAddress), + } + resp, err := nhc.CreateNextHop(context.Background(), req) + if err != nil { + t.Fatalf("failed to create next hop %d: %v", id, err) + } + s.oc2SAINextHop[id] = resp.GetOid() + } + + for id, nhg := range aft.NextHopGroup { + req := &saipb.CreateNextHopGroupRequest{ + Type: saipb.NextHopGroupType_NEXT_HOP_GROUP_TYPE_ECMP_WITH_MEMBERS.Enum(), + } + for id, nh := range nhg.NextHop { + req.NextHopList = append(req.NextHopList, s.oc2SAINextHop[id]) + req.NextHopMemberWeightList = append(req.NextHopMemberWeightList, uint32(nh.GetWeight())) + } + resp, err := nhgc.CreateNextHopGroup(context.Background(), req) + if err != nil { + t.Fatalf("failed to create next hop group %d: %v", id, err) + } + s.oc2SAINextHopGrp[id] = resp.GetOid() + } + + for pre, entry := range aft.Ipv4Entry { + CreateRoute(t, dut, pre, s.oc2SAINextHopGrp[entry.GetNextHopGroup()], s.oc2SAIVRF[vrf]) + } + for pre, entry := range aft.Ipv6Entry { + CreateRoute(t, dut, pre, s.oc2SAINextHopGrp[entry.GetNextHopGroup()], s.oc2SAIVRF[vrf]) + } + } + c.uncfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) {} + + return c +} + +func ConfigRIF(portID string, mac string, vlan string) *configer { + c := &configer{} + + c.cfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + id := CreateRIF(t, dut, dut.Port(t, portID), mac) + s.interfaceMap[InterfaceRef{Intf: portID}] = id + } + + c.uncfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) {} + return c +} + +func ConfigVLANSubIntf(portID string, index uint32, smac string, vlan uint16, vrf string) *configer { + c := &configer{} + + c.cfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + conn := dataplaneConn(t, dut) + ric := saipb.NewRouterInterfaceClient(conn) + port1ID, err := strconv.ParseUint(dut.Port(t, portID).Name(), 10, 64) + if err != nil { + t.Fatal(err) + } + mac, err := net.ParseMAC(smac) + if err != nil { + t.Fatal(err) + } + + resp, err := ric.CreateRouterInterface(context.Background(), &saipb.CreateRouterInterfaceRequest{ + Switch: 1, + PortId: proto.Uint64(port1ID), + Type: saipb.RouterInterfaceType_ROUTER_INTERFACE_TYPE_SUB_PORT.Enum(), + OuterVlanId: proto.Uint32(uint32(vlan)), + SrcMacAddress: mac, + }) + if err != nil { + t.Fatal(err) + } + s.interfaceMap[InterfaceRef{Intf: portID, SubIntf: index}] = resp.Oid + } + + c.uncfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) {} + return c +} + +func ConfigNeighbor(intf InterfaceRef, ip, mac string) *configer { + c := &configer{} + + c.cfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + CreateNeighbor(t, dut, ip, mac, s.interfaceMap[intf]) + } + + c.uncfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) {} + return c +} + +func ConfigVRF(name string) *configer { + c := &configer{ + cfg: func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) { + conn := dataplaneConn(t, dut) + vrc := saipb.NewVirtualRouterClient(conn) + vrc.CreateVirtualRouter(context.Background(), &saipb.CreateVirtualRouterRequest{ + Switch: 1, + }) + }, + } + c.uncfg = func(t testing.TB, s *Suite, dut *ondatra.DUTDevice) {} + return c +} + +func MustParseMac(t testing.TB, mac string) net.HardwareAddr { + addr, err := net.ParseMAC(mac) + if err != nil { + t.Fatal(err) + } + return addr +}