diff --git a/dataplane/saiserver/BUILD b/dataplane/saiserver/BUILD index c4d44370..4bae291e 100644 --- a/dataplane/saiserver/BUILD +++ b/dataplane/saiserver/BUILD @@ -4,6 +4,7 @@ go_library( name = "saiserver", srcs = [ "acl.go", + "debug_counter.go", "fdb.go", "hostif.go", "isolation_group.go", diff --git a/dataplane/saiserver/debug_counter.go b/dataplane/saiserver/debug_counter.go new file mode 100644 index 00000000..c7a2b1db --- /dev/null +++ b/dataplane/saiserver/debug_counter.go @@ -0,0 +1,68 @@ +// 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" + + saipb "github.com/openconfig/lemming/dataplane/proto/sai" + "github.com/openconfig/lemming/dataplane/saiserver/attrmgr" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" +) + +type debugCounter struct { + saipb.UnimplementedDebugCounterServer + mgr *attrmgr.AttrMgr + dataplane switchDataplaneAPI +} + +func newDebugCounter(mgr *attrmgr.AttrMgr, engine switchDataplaneAPI, s *grpc.Server) *debugCounter { + d := &debugCounter{ + mgr: mgr, + dataplane: engine, + } + saipb.RegisterDebugCounterServer(s, d) + return d +} + +func (d *debugCounter) CreateDebugCounter(ctx context.Context, req *saipb.CreateDebugCounterRequest) (*saipb.CreateDebugCounterResponse, error) { + oid := d.mgr.NextID() + index := uint32(0) + for _, reason := range req.InDropReasonList { + if reason == saipb.InDropReason_IN_DROP_REASON_LPM4_MISS { + index = 0 + break + } + if reason == saipb.InDropReason_IN_DROP_REASON_LPM6_MISS { + index = 1 + break + } + } + + d.mgr.StoreAttributes(oid, &saipb.DebugCounterAttribute{ + Index: proto.Uint32(index), + }) + return &saipb.CreateDebugCounterResponse{Oid: oid}, nil +} + +func (d *debugCounter) RemoveDebugCounter(ctx context.Context, req *saipb.RemoveDebugCounterRequest) (*saipb.RemoveDebugCounterResponse, error) { + return &saipb.RemoveDebugCounterResponse{}, nil +} + +func (d *debugCounter) SetDebugCounterAttribute(ctx context.Context, req *saipb.SetDebugCounterAttributeRequest) (*saipb.SetDebugCounterAttributeResponse, error) { + return &saipb.SetDebugCounterAttributeResponse{}, nil +} + diff --git a/dataplane/saiserver/routing.go b/dataplane/saiserver/routing.go index 5ea6db0a..2ad1f061 100644 --- a/dataplane/saiserver/routing.go +++ b/dataplane/saiserver/routing.go @@ -704,6 +704,21 @@ func (r *route) CreateRouteEntry(ctx context.Context, req *saipb.CreateRouteEntr } } else { actions = append(actions, fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_BIT_WRITE, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ACTION).WithBitOp(1, 0).WithValue([]byte{0})) + + isZeroMask := true + for _, b := range req.GetEntry().GetDestination().GetMask() { + if b != 0 { + isZeroMask = false + break + } + } + if isZeroMask { + counterID := "LPM6_MISS_COUNTER" + if fib == FIBV4Table { + counterID = "LPM4_MISS_COUNTER" + } + actions = append(actions, fwdconfig.FlowCounterAction(counterID)) + } } if req.MetaData != nil { actions = append(actions, fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_SET, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ATTRIBUTE_32). @@ -829,6 +844,21 @@ func (r *route) SetRouteEntryAttribute(ctx context.Context, req *saipb.SetRouteE } } else { actions = append(actions, fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_BIT_WRITE, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ACTION).WithBitOp(1, 0).WithValue([]byte{0})) + + isZeroMask := true + for _, b := range req.GetEntry().GetDestination().GetMask() { + if b != 0 { + isZeroMask = false + break + } + } + if isZeroMask { + counterID := "LPM6_MISS_COUNTER" + if fib == FIBV4Table { + counterID = "LPM4_MISS_COUNTER" + } + actions = append(actions, fwdconfig.FlowCounterAction(counterID)) + } } if metaData != nil { actions = append(actions, fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_SET, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ATTRIBUTE_32). diff --git a/dataplane/saiserver/saiserver.go b/dataplane/saiserver/saiserver.go index 84de2453..15c8bc59 100644 --- a/dataplane/saiserver/saiserver.go +++ b/dataplane/saiserver/saiserver.go @@ -39,9 +39,6 @@ type counter struct { saipb.UnimplementedCounterServer } -type debugCounter struct { - saipb.UnimplementedDebugCounterServer -} type dtel struct { saipb.UnimplementedDtelServer @@ -209,7 +206,7 @@ func New(ctx context.Context, mgr *attrmgr.AttrMgr, s *grpc.Server, opts *dplane forwardingContext: fwdCtx, bfd: &bfd{}, counter: &counter{}, - debugCounter: &debugCounter{}, + debugCounter: newDebugCounter(mgr, fwdCtx, s), dtel: &dtel{}, fdb: fdb, ipmcGroup: &ipmcGroup{}, @@ -232,7 +229,6 @@ func New(ctx context.Context, mgr *attrmgr.AttrMgr, s *grpc.Server, opts *dplane saipb.RegisterEntrypointServer(s, srv) saipb.RegisterBfdServer(s, srv.bfd) saipb.RegisterCounterServer(s, srv.counter) - saipb.RegisterDebugCounterServer(s, srv.debugCounter) saipb.RegisterDtelServer(s, srv.dtel) saipb.RegisterIpmcGroupServer(s, srv.ipmcGroup) saipb.RegisterIpmcServer(s, srv.ipmc) diff --git a/dataplane/saiserver/switch.go b/dataplane/saiserver/switch.go index 0baa3d54..88a8ece8 100644 --- a/dataplane/saiserver/switch.go +++ b/dataplane/saiserver/switch.go @@ -268,6 +268,33 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ swID := sw.mgr.NextID() slog.InfoContext(ctx, "Creating new switch id", "id", swID) + // Create LPM miss flow counters if they don't exist. + qResp, err := sw.dataplane.FlowCounterQuery(ctx, &fwdpb.FlowCounterQueryRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Ids: []*fwdpb.FlowCounterId{{ObjectId: &fwdpb.ObjectId{Id: "LPM4_MISS_COUNTER"}}}, + }) + if err != nil || len(qResp.GetCounters()) == 0 { + if _, err := sw.dataplane.FlowCounterCreate(ctx, &fwdpb.FlowCounterCreateRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Id: &fwdpb.FlowCounterId{ObjectId: &fwdpb.ObjectId{Id: "LPM4_MISS_COUNTER"}}, + }); err != nil { + return nil, err + } + } + + qResp6, err := sw.dataplane.FlowCounterQuery(ctx, &fwdpb.FlowCounterQueryRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Ids: []*fwdpb.FlowCounterId{{ObjectId: &fwdpb.ObjectId{Id: "LPM6_MISS_COUNTER"}}}, + }) + if err != nil || len(qResp6.GetCounters()) == 0 { + if _, err := sw.dataplane.FlowCounterCreate(ctx, &fwdpb.FlowCounterCreateRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Id: &fwdpb.FlowCounterId{ObjectId: &fwdpb.ObjectId{Id: "LPM6_MISS_COUNTER"}}, + }); err != nil { + return nil, err + } + } + // Setup forwarding tables. ingressVRF := &fwdpb.TableCreateRequest{ ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, @@ -295,7 +322,17 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ Desc: &fwdpb.TableDesc{ TableType: fwdpb.TableType_TABLE_TYPE_PREFIX, TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: FIBV4Table}}, - Actions: []*fwdpb.ActionDesc{fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_BIT_WRITE, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ACTION).WithBitOp(1, 0).WithValue([]byte{0})).Build()}, + Actions: []*fwdpb.ActionDesc{ + fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_BIT_WRITE, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ACTION).WithBitOp(1, 0).WithValue([]byte{0})).Build(), + { + ActionType: fwdpb.ActionType_ACTION_TYPE_FLOW_COUNTER, + Action: &fwdpb.ActionDesc_Flow{ + Flow: &fwdpb.FlowCounterActionDesc{ + CounterId: &fwdpb.FlowCounterId{ObjectId: &fwdpb.ObjectId{Id: "LPM4_MISS_COUNTER"}}, + }, + }, + }, + }, Table: &fwdpb.TableDesc_Prefix{ Prefix: &fwdpb.PrefixTableDesc{ FieldIds: []*fwdpb.PacketFieldId{{ @@ -319,7 +356,17 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ Desc: &fwdpb.TableDesc{ TableType: fwdpb.TableType_TABLE_TYPE_PREFIX, TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: FIBV6Table}}, - Actions: []*fwdpb.ActionDesc{fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_BIT_WRITE, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ACTION).WithBitOp(1, 0).WithValue([]byte{0})).Build()}, + Actions: []*fwdpb.ActionDesc{ + fwdconfig.Action(fwdconfig.UpdateAction(fwdpb.UpdateType_UPDATE_TYPE_BIT_WRITE, fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_ACTION).WithBitOp(1, 0).WithValue([]byte{0})).Build(), + { + ActionType: fwdpb.ActionType_ACTION_TYPE_FLOW_COUNTER, + Action: &fwdpb.ActionDesc_Flow{ + Flow: &fwdpb.FlowCounterActionDesc{ + CounterId: &fwdpb.FlowCounterId{ObjectId: &fwdpb.ObjectId{Id: "LPM6_MISS_COUNTER"}}, + }, + }, + }, + }, Table: &fwdpb.TableDesc_Prefix{ Prefix: &fwdpb.PrefixTableDesc{ FieldIds: []*fwdpb.PacketFieldId{{ @@ -515,7 +562,7 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ if _, err := sw.dataplane.TableCreate(ctx, nexthopAction); err != nil { return nil, err } - _, err := sw.dataplane.TableCreate(ctx, &fwdpb.TableCreateRequest{ + _, err = sw.dataplane.TableCreate(ctx, &fwdpb.TableCreateRequest{ ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, Desc: &fwdpb.TableDesc{ TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: portToHostifTable}}, @@ -875,6 +922,26 @@ func (sw *saiSwitch) CreateSwitch(ctx context.Context, _ *saipb.CreateSwitchRequ SwitchShellEnable: proto.Bool(false), SwitchProfileId: proto.Uint32(0), NatZoneCounterObjectId: proto.Uint64(0), + SupportedObjectTypeList: []saipb.ObjectType{ + saipb.ObjectType_OBJECT_TYPE_PORT, + saipb.ObjectType_OBJECT_TYPE_VLAN, + saipb.ObjectType_OBJECT_TYPE_VIRTUAL_ROUTER, + saipb.ObjectType_OBJECT_TYPE_NEXT_HOP, + saipb.ObjectType_OBJECT_TYPE_NEXT_HOP_GROUP, + saipb.ObjectType_OBJECT_TYPE_ROUTE_ENTRY, + saipb.ObjectType_OBJECT_TYPE_FDB_ENTRY, + saipb.ObjectType_OBJECT_TYPE_ACL_TABLE, + saipb.ObjectType_OBJECT_TYPE_ACL_ENTRY, + saipb.ObjectType_OBJECT_TYPE_DEBUG_COUNTER, + }, + SupportedDebugCounterTypeList: []saipb.DebugCounterType{ + saipb.DebugCounterType_DEBUG_COUNTER_TYPE_SWITCH_IN_DROP_REASONS, + }, + SupportedIngressDropReasonList: []saipb.InDropReason{ + saipb.InDropReason_IN_DROP_REASON_LPM4_MISS, + saipb.InDropReason_IN_DROP_REASON_LPM6_MISS, + }, + AvailableSwitchIngressDropCounters: proto.Uint32(2), } sw.mgr.StoreAttributes(swID, attrs) return &saipb.CreateSwitchResponse{ @@ -1136,6 +1203,50 @@ func (sw *saiSwitch) Reset() { sw.hostif.Reset() } +// GetSwitchStats returns the statistics for the switch. +func (sw *saiSwitch) GetSwitchStats(ctx context.Context, req *saipb.GetSwitchStatsRequest) (*saipb.GetSwitchStatsResponse, error) { + slog.ErrorContext(ctx, "GetSwitchStats called", "req", req) + resp := &saipb.GetSwitchStatsResponse{} + for _, id := range req.GetCounterIds() { + var val uint64 + switch id { + case saipb.SwitchStat_SWITCH_STAT_IN_CONFIGURED_DROP_REASONS_0_DROPPED_PKTS: + count, err := sw.dataplane.FlowCounterQuery(ctx, &fwdpb.FlowCounterQueryRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Ids: []*fwdpb.FlowCounterId{{ObjectId: &fwdpb.ObjectId{Id: "LPM4_MISS_COUNTER"}}}, + }) + if err != nil { + return nil, err + } + if len(count.GetCounters()) > 0 { + val = count.GetCounters()[0].GetPackets() + slog.ErrorContext(ctx, "GetSwitchStats: LPM4_MISS_COUNTER", "packets", val) + } else { + slog.InfoContext(ctx, "GetSwitchStats: LPM4_MISS_COUNTER not found in response") + } + case saipb.SwitchStat_SWITCH_STAT_IN_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS: + count, err := sw.dataplane.FlowCounterQuery(ctx, &fwdpb.FlowCounterQueryRequest{ + ContextId: &fwdpb.ContextId{Id: sw.dataplane.ID()}, + Ids: []*fwdpb.FlowCounterId{{ObjectId: &fwdpb.ObjectId{Id: "LPM6_MISS_COUNTER"}}}, + }) + if err != nil { + return nil, err + } + if len(count.GetCounters()) > 0 { + val = count.GetCounters()[0].GetPackets() + slog.ErrorContext(ctx, "GetSwitchStats: LPM6_MISS_COUNTER", "packets", val) + } else { + slog.InfoContext(ctx, "GetSwitchStats: LPM6_MISS_COUNTER not found in response") + } + default: + val = 0 + } + resp.Values = append(resp.Values, val) + } + + return resp, nil +} + // createFIBSelector creates a table that controls which forwarding table is used. func (sw *saiSwitch) createFIBSelector(ctx context.Context) error { fieldID := &fwdpb.PacketFieldId{ diff --git a/dataplane/saiserver/switch_test.go b/dataplane/saiserver/switch_test.go index c35fe1b4..9653846c 100644 --- a/dataplane/saiserver/switch_test.go +++ b/dataplane/saiserver/switch_test.go @@ -132,6 +132,26 @@ func TestCreateSwitch(t *testing.T) { SwitchShellEnable: proto.Bool(false), SwitchProfileId: proto.Uint32(0), NatZoneCounterObjectId: proto.Uint64(0), + SupportedObjectTypeList: []saipb.ObjectType{ + saipb.ObjectType_OBJECT_TYPE_PORT, + saipb.ObjectType_OBJECT_TYPE_VLAN, + saipb.ObjectType_OBJECT_TYPE_VIRTUAL_ROUTER, + saipb.ObjectType_OBJECT_TYPE_NEXT_HOP, + saipb.ObjectType_OBJECT_TYPE_NEXT_HOP_GROUP, + saipb.ObjectType_OBJECT_TYPE_ROUTE_ENTRY, + saipb.ObjectType_OBJECT_TYPE_FDB_ENTRY, + saipb.ObjectType_OBJECT_TYPE_ACL_TABLE, + saipb.ObjectType_OBJECT_TYPE_ACL_ENTRY, + saipb.ObjectType_OBJECT_TYPE_DEBUG_COUNTER, + }, + SupportedDebugCounterTypeList: []saipb.DebugCounterType{ + saipb.DebugCounterType_DEBUG_COUNTER_TYPE_SWITCH_IN_DROP_REASONS, + }, + SupportedIngressDropReasonList: []saipb.InDropReason{ + saipb.InDropReason_IN_DROP_REASON_LPM4_MISS, + saipb.InDropReason_IN_DROP_REASON_LPM6_MISS, + }, + AvailableSwitchIngressDropCounters: proto.Uint32(2), } attr := &saipb.SwitchAttribute{} if err := mgr.PopulateAllAttributes("1", attr); err != nil { @@ -326,8 +346,8 @@ func (f *fakeSwitchDataplane) FlowCounterCreate(_ context.Context, req *fwdpb.Fl func (f *fakeSwitchDataplane) FlowCounterQuery(_ context.Context, req *fwdpb.FlowCounterQueryRequest) (*fwdpb.FlowCounterQueryReply, error) { f.gotFlowCounterQueryReqs = append(f.gotFlowCounterQueryReqs, req) - if f.flowQueryRepliesIdx > len(f.flowQueryReplies) { - return nil, io.EOF + if f.flowQueryRepliesIdx >= len(f.flowQueryReplies) { + return &fwdpb.FlowCounterQueryReply{}, nil } r := f.flowQueryReplies[f.flowQueryRepliesIdx] f.flowQueryRepliesIdx++ @@ -363,3 +383,114 @@ func newTestSwitch(t testing.TB, dplane switchDataplaneAPI) (saipb.SwitchClient, }) return saipb.NewSwitchClient(conn), mgr, stopFn } + +func TestGetSwitchStats(t *testing.T) { + tests := []struct { + desc string + req *saipb.GetSwitchStatsRequest + replies []*fwdpb.FlowCounterQueryReply + want *saipb.GetSwitchStatsResponse + wantErr string + }{{ + desc: "LPM4 miss counter", + req: &saipb.GetSwitchStatsRequest{ + CounterIds: []saipb.SwitchStat{saipb.SwitchStat_SWITCH_STAT_IN_CONFIGURED_DROP_REASONS_0_DROPPED_PKTS}, + }, + replies: []*fwdpb.FlowCounterQueryReply{{ + Counters: []*fwdpb.FlowCounter{{ + Packets: 10, + }}, + }}, + want: &saipb.GetSwitchStatsResponse{ + Values: []uint64{10}, + }, + }, { + desc: "LPM6 miss counter", + req: &saipb.GetSwitchStatsRequest{ + CounterIds: []saipb.SwitchStat{saipb.SwitchStat_SWITCH_STAT_IN_CONFIGURED_DROP_REASONS_1_DROPPED_PKTS}, + }, + replies: []*fwdpb.FlowCounterQueryReply{{ + Counters: []*fwdpb.FlowCounter{{ + Packets: 20, + }}, + }}, + want: &saipb.GetSwitchStatsResponse{ + Values: []uint64{20}, + }, + }, { + desc: "unknown counter", + req: &saipb.GetSwitchStatsRequest{ + CounterIds: []saipb.SwitchStat{saipb.SwitchStat_SWITCH_STAT_ECC_DROP}, + }, + replies: []*fwdpb.FlowCounterQueryReply{}, + want: &saipb.GetSwitchStatsResponse{ + Values: []uint64{0}, + }, + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + dplane := &fakeSwitchDataplane{ + flowQueryReplies: tt.replies, + } + c, _, stopFn := newTestSwitch(t, dplane) + defer stopFn() + + got, gotErr := c.GetSwitchStats(context.TODO(), tt.req) + if diff := errdiff.Check(gotErr, tt.wantErr); diff != "" { + t.Fatalf("GetSwitchStats() unexpected err: %s", diff) + } + if gotErr != nil { + return + } + if d := cmp.Diff(got, tt.want, protocmp.Transform()); d != "" { + t.Errorf("GetSwitchStats() failed: diff(-got,+want)\n:%s", d) + } + }) + } +} + +// TestLpmMissCountersScenario emulates the scenario where a packet is sent to an address different than any installed route, +// causing an ALPM miss which is tracked by the miss counters. +func TestLpmMissCountersScenario(t *testing.T) { + dplane := &fakeSwitchDataplane{} + c, _, stopFn := newTestSwitch(t, dplane) + defer stopFn() + + _, err := c.CreateSwitch(context.TODO(), &saipb.CreateSwitchRequest{}) + if err != nil { + t.Fatalf("CreateSwitch() failed: %v", err) + } + + // Verify counters were created. + var gotV4, gotV6 bool + for _, req := range dplane.gotFlowCounterCreateReqs { + if req.GetId().GetObjectId().GetId() == "LPM4_MISS_COUNTER" { + gotV4 = true + } + if req.GetId().GetObjectId().GetId() == "LPM6_MISS_COUNTER" { + gotV6 = true + } + } + if !gotV4 { + t.Errorf("LPM4_MISS_COUNTER not created") + } + if !gotV6 { + t.Errorf("LPM6_MISS_COUNTER not created") + } + + // Verify we can query stats. + dplane.flowQueryReplies = []*fwdpb.FlowCounterQueryReply{{ + Counters: []*fwdpb.FlowCounter{{Packets: 42}}, + }} + dplane.flowQueryRepliesIdx = 0 + + stats, err := c.GetSwitchStats(context.TODO(), &saipb.GetSwitchStatsRequest{ + CounterIds: []saipb.SwitchStat{saipb.SwitchStat_SWITCH_STAT_IN_CONFIGURED_DROP_REASONS_0_DROPPED_PKTS}, + }) + if err != nil { + t.Fatalf("GetSwitchStats() failed: %v", err) + } + if len(stats.GetValues()) == 0 || stats.GetValues()[0] != 42 { + t.Errorf("Expected 42, got %v", stats.GetValues()) + } +}