From e76898896b0277279ddc5d624207f9f265c4282d Mon Sep 17 00:00:00 2001 From: Rob Shakir Date: Fri, 15 Sep 2023 23:34:48 +0000 Subject: [PATCH 1/2] Add support for Get of MPLS entries to RIB. * (M) rib/rib(_test)?.go - Add support for MPLS entries being returned in a RIB `Get` response. --- rib/rib.go | 52 +++++++++++++++++++++ rib/rib_test.go | 117 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/rib/rib.go b/rib/rib.go index 4f094772..b0b17a51 100644 --- a/rib/rib.go +++ b/rib/rib.go @@ -1744,6 +1744,35 @@ func concreteIPv4Proto(e *aft.Afts_Ipv4Entry) (*aftpb.Afts_Ipv4EntryKey, error) }, nil } +// concreteMPLSProto takes the input LabelEntry GoStruct and returns it as a gRIBI +// LabelEntryKey protobuf. It returns an error if the protobuf cannot be marshalled. +func concreteMPLSProto(e *aft.Afts_LabelEntry) (*aftpb.Afts_LabelEntryKey, error) { + mplsProto := &aftpb.Afts_LabelEntry{} + if err := protoFromGoStruct(e, &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "afts", + }, { + Name: "mpls", + }, { + Name: "label-entry", + }}, + }, mplsProto); err != nil { + return nil, fmt.Errorf("cannot marshal MPLS label %v, %v", e.GetLabel(), err) + } + + l, ok := e.GetLabel().(aft.UnionUint32) + if !ok { + return nil, fmt.Errorf("cannot marshal MPLS label %v, incorrect type %T", e.GetLabel(), e.GetLabel()) + } + + return &aftpb.Afts_LabelEntryKey{ + Label: &aftpb.Afts_LabelEntryKey_LabelUint64{ + LabelUint64: uint64(l), + }, + LabelEntry: mplsProto, + }, nil +} + // concreteNextHopProto takes the input NextHop GoStruct and returns it as a gRIBI // NextHopEntryKey protobuf. It returns an error if the protobuf cannot be marshalled. func concreteNextHopProto(e *aft.Afts_NextHop) (*aftpb.Afts_NextHopKey, error) { @@ -1844,6 +1873,7 @@ func (r *RIBHolder) GetRIB(filter map[spb.AFTType]bool, msgCh chan *spb.GetRespo if filter[spb.AFTType_ALL] { filter = map[spb.AFTType]bool{ spb.AFTType_IPV4: true, + spb.AFTType_MPLS: true, spb.AFTType_NEXTHOP: true, spb.AFTType_NEXTHOP_GROUP: true, } @@ -1871,6 +1901,28 @@ func (r *RIBHolder) GetRIB(filter map[spb.AFTType]bool, msgCh chan *spb.GetRespo } } + if filter[spb.AFTType_MPLS] { + for lbl, e := range r.r.Afts.LabelEntry { + select { + case <-stopCh: + return nil + default: + p, err := concreteMPLSProto(e) + if err != nil { + return status.Errorf(codes.Internal, "cannot marshal MPLS entry for label %d into GetResponse, %v", lbl, err) + } + msgCh <- &spb.GetResponse{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: r.name, + Entry: &spb.AFTEntry_Mpls{ + Mpls: p, + }, + }}, + } + } + } + } + if filter[spb.AFTType_NEXTHOP_GROUP] { for index, e := range r.r.Afts.NextHopGroup { select { diff --git a/rib/rib_test.go b/rib/rib_test.go index 636c10af..73040268 100644 --- a/rib/rib_test.go +++ b/rib/rib_test.go @@ -886,7 +886,61 @@ func TestConcreteIPv4Proto(t *testing.T) { t.Run(tt.desc, func(t *testing.T) { got, err := concreteIPv4Proto(tt.inEntry) if (err != nil) != tt.wantErr { - t.Fatalf("did not get expec ted error, got: %v, wantErr? %v", err, tt.wantErr) + t.Fatalf("did not get expected error, got: %v, wantErr? %v", err, tt.wantErr) + } + if diff := cmp.Diff(got, tt.want, protocmp.Transform(), cmpopts.EquateEmpty()); diff != "" { + t.Fatalf("did not get expected proto, diff(-got,+want):\n%s", diff) + } + }) + } +} + +func TestConcreteMPLSProto(t *testing.T) { + tests := []struct { + desc string + inEntry *aft.Afts_LabelEntry + want *aftpb.Afts_LabelEntryKey + wantErr bool + }{{ + desc: "label only", + inEntry: &aft.Afts_LabelEntry{ + Label: aft.UnionUint32(42), + }, + want: &aftpb.Afts_LabelEntryKey{ + Label: &aftpb.Afts_LabelEntryKey_LabelUint64{ + LabelUint64: 42, + }, + LabelEntry: &aftpb.Afts_LabelEntry{}, + }, + }, { + desc: "enumerated label", + inEntry: &aft.Afts_LabelEntry{ + Label: aft.MplsTypes_MplsLabel_Enum_IPV4_EXPLICIT_NULL, // not allowed + }, + wantErr: true, + }, { + desc: "label with metadata set", + inEntry: &aft.Afts_LabelEntry{ + Label: aft.UnionUint32(42), + EntryMetadata: []byte{4, 3, 2, 1}, + }, + want: &aftpb.Afts_LabelEntryKey{ + Label: &aftpb.Afts_LabelEntryKey_LabelUint64{ + LabelUint64: 42, + }, + LabelEntry: &aftpb.Afts_LabelEntry{ + EntryMetadata: &wpb.BytesValue{ + Value: []byte{4, 3, 2, 1}, + }, + }, + }, + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + got, err := concreteMPLSProto(tt.inEntry) + if (err != nil) != tt.wantErr { + t.Fatalf("did not get expected error, got: %v, wantErr? %v", err, tt.wantErr) } if diff := cmp.Diff(got, tt.want, protocmp.Transform(), cmpopts.EquateEmpty()); diff != "" { t.Fatalf("did not get expected proto, diff(-got,+want):\n%s", diff) @@ -2925,6 +2979,14 @@ func TestGetRIB(t *testing.T) { t.Fatalf("cannot build RIB, %v", err) } + cr = &aft.RIB{} + mpls := cr.GetOrCreateAfts().GetOrCreateLabelEntry(aft.UnionUint32(42)) + mpls.NextHopGroup = ygot.Uint64(42) + + if _, err := r.doAddMPLS(42, cr); err != nil { + t.Fatalf("cannot build RIB, %v", err) + } + return r }() @@ -2971,6 +3033,38 @@ func TestGetRIB(t *testing.T) { }, }}, }}, + }, { + desc: "mpls entry", + inRIB: func() *RIBHolder { + r := NewRIBHolder("VRF-1") + + cr := &aft.RIB{} + mpls := cr.GetOrCreateAfts().GetOrCreateLabelEntry(aft.UnionUint32(42)) + mpls.NextHopGroup = ygot.Uint64(42) + + if _, err := r.doAddMPLS(42, cr); err != nil { + t.Fatalf("cannot build RIB, %v", err) + } + return r + }(), + inFilter: map[spb.AFTType]bool{ + spb.AFTType_ALL: true, + }, + wantResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: "VRF-1", + Entry: &spb.AFTEntry_Mpls{ + Mpls: &aftpb.Afts_LabelEntryKey{ + Label: &aftpb.Afts_LabelEntryKey_LabelUint64{ + LabelUint64: 42, + }, + LabelEntry: &aftpb.Afts_LabelEntry{ + NextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }}, + }}, }, { desc: "next-hop-group entry", inRIB: func() *RIBHolder { @@ -3050,6 +3144,27 @@ func TestGetRIB(t *testing.T) { }, }}, }}, + }, { + desc: "all tables populated but filtered to mpls", + inRIB: allPopRIB, + inFilter: map[spb.AFTType]bool{ + spb.AFTType_MPLS: true, + }, + wantResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: "VRF-42", + Entry: &spb.AFTEntry_Mpls{ + Mpls: &aftpb.Afts_LabelEntryKey{ + Label: &aftpb.Afts_LabelEntryKey_LabelUint64{ + LabelUint64: 42, + }, + LabelEntry: &aftpb.Afts_LabelEntry{ + NextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }}, + }}, }, { desc: "all tables populated but filtered to nhg", inRIB: allPopRIB, From 531618455ff60a9589b1becf54bc75b46a0f969d Mon Sep 17 00:00:00 2001 From: Rob Shakir Date: Mon, 18 Sep 2023 14:02:50 -0700 Subject: [PATCH 2/2] Add support for building a RIB from a gRIBI.Get response. (#193) * Add support for building a RIB from a gRIBI.Get response. * (A) rib/helpers(_test)?.go - Add a new function that allows a RIB to be built from a slice of gRIBI GetResponse messages that are received from a server. This allows a RIB to be built from an external source. * run gofmt. --- rib/helpers.go | 72 +++++++++++++ rib/helpers_test.go | 258 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 rib/helpers.go create mode 100644 rib/helpers_test.go diff --git a/rib/helpers.go b/rib/helpers.go new file mode 100644 index 00000000..836c87f6 --- /dev/null +++ b/rib/helpers.go @@ -0,0 +1,72 @@ +// Copyright 2023 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 rib + +import ( + "fmt" + + aftpb "github.com/openconfig/gribi/v1/proto/gribi_aft" + spb "github.com/openconfig/gribi/v1/proto/service" + "github.com/openconfig/ygot/ygot" +) + +// RIBFromGetResponses returns a RIB from a slice of gRIBI GetResponse messages. +// The supplied defaultName is used as the default network instance name. +func RIBFromGetResponses(defaultName string, responses []*spb.GetResponse) (*RIB, error) { + r := New(defaultName) + niAFTs := map[string]*aftpb.Afts{} + + for _, resp := range responses { + for _, e := range resp.Entry { + ni := e.GetNetworkInstance() + if _, ok := niAFTs[ni]; !ok { + niAFTs[ni] = &aftpb.Afts{} + } + switch t := e.GetEntry().(type) { + case *spb.AFTEntry_Ipv4: + niAFTs[ni].Ipv4Entry = append(niAFTs[ni].Ipv4Entry, t.Ipv4) + case *spb.AFTEntry_Mpls: + niAFTs[ni].LabelEntry = append(niAFTs[ni].LabelEntry, t.Mpls) + case *spb.AFTEntry_NextHopGroup: + niAFTs[ni].NextHopGroup = append(niAFTs[ni].NextHopGroup, t.NextHopGroup) + case *spb.AFTEntry_NextHop: + niAFTs[ni].NextHop = append(niAFTs[ni].NextHop, t.NextHop) + default: + return nil, fmt.Errorf("unknown/unhandled type %T in received GetResponses", t) + } + } + } + + // Throughout this operation we don't worry about locking the structures in the new + // RIB because we are the only function that can access the newly created data structure. + for ni, niRIB := range niAFTs { + cr, err := candidateRIB(niRIB) + if err != nil { + return nil, fmt.Errorf("cannot build RIB for NI %s, err: %v", ni, err) + } + + if ni != defaultName { + if err := r.AddNetworkInstance(ni); err != nil { + return nil, fmt.Errorf("cannot create network instance RIB for NI %s, err: %v", ni, err) + } + } + + if err := ygot.MergeStructInto(r.niRIB[ni].r, cr); err != nil { + return nil, fmt.Errorf("cannot populate network instance RIB for NI %s, err: %v", ni, err) + } + } + + return r, nil +} diff --git a/rib/helpers_test.go b/rib/helpers_test.go new file mode 100644 index 00000000..cb0fb461 --- /dev/null +++ b/rib/helpers_test.go @@ -0,0 +1,258 @@ +// Copyright 2021 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 rib + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/openconfig/gribigo/aft" + + aftpb "github.com/openconfig/gribi/v1/proto/gribi_aft" + spb "github.com/openconfig/gribi/v1/proto/service" + wpb "github.com/openconfig/ygot/proto/ywrapper" + "github.com/openconfig/ygot/ygot" +) + +func TestRIBFromGetResponses(t *testing.T) { + defaultName := "DEFAULT" + tests := []struct { + desc string + inDefaultName string + inResponses []*spb.GetResponse + wantRIB *RIB + wantErr bool + }{{ + desc: "single ipv4 entry", + inResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: "VRF-1", + Entry: &spb.AFTEntry_Ipv4{ + Ipv4: &aftpb.Afts_Ipv4EntryKey{ + Prefix: "1.1.1.1/32", + Ipv4Entry: &aftpb.Afts_Ipv4Entry{ + NextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }}, + }}, + inDefaultName: "DEFAULT", + wantRIB: &RIB{ + defaultName: defaultName, + niRIB: map[string]*RIBHolder{ + "DEFAULT": { + name: "DEFAULT", + r: &aft.RIB{ + Afts: &aft.Afts{}, + }, + }, + "VRF-1": { + name: "VRF-1", + r: &aft.RIB{ + Afts: &aft.Afts{ + Ipv4Entry: map[string]*aft.Afts_Ipv4Entry{ + "1.1.1.1/32": { + Prefix: ygot.String("1.1.1.1/32"), + NextHopGroup: ygot.Uint64(42), + }, + }, + }, + }, + }, + }, + }, + }, { + desc: "next-hop group entry", + inResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: defaultName, + Entry: &spb.AFTEntry_NextHopGroup{ + NextHopGroup: &aftpb.Afts_NextHopGroupKey{ + Id: 42, + NextHopGroup: &aftpb.Afts_NextHopGroup{ + BackupNextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }}, + }}, + inDefaultName: "DEFAULT", + wantRIB: &RIB{ + defaultName: defaultName, + niRIB: map[string]*RIBHolder{ + "DEFAULT": { + name: "DEFAULT", + r: &aft.RIB{ + Afts: &aft.Afts{ + NextHopGroup: map[uint64]*aft.Afts_NextHopGroup{ + 42: { + Id: ygot.Uint64(42), + BackupNextHopGroup: ygot.Uint64(42), + }, + }, + }, + }, + }, + }, + }, + }, { + desc: "nexthop entry", + inResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: defaultName, + Entry: &spb.AFTEntry_NextHop{ + NextHop: &aftpb.Afts_NextHopKey{ + Index: 42, + NextHop: &aftpb.Afts_NextHop{ + IpAddress: &wpb.StringValue{Value: "1.1.1.1"}, + }, + }, + }, + }}, + }}, + inDefaultName: "DEFAULT", + wantRIB: &RIB{ + defaultName: defaultName, + niRIB: map[string]*RIBHolder{ + "DEFAULT": { + name: "DEFAULT", + r: &aft.RIB{ + Afts: &aft.Afts{ + NextHop: map[uint64]*aft.Afts_NextHop{ + 42: { + Index: ygot.Uint64(42), + IpAddress: ygot.String("1.1.1.1"), + }, + }, + }, + }, + }, + }, + }, + }, { + desc: "mpls entry", + inResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: defaultName, + Entry: &spb.AFTEntry_Mpls{ + Mpls: &aftpb.Afts_LabelEntryKey{ + Label: &aftpb.Afts_LabelEntryKey_LabelUint64{ + LabelUint64: 42, + }, + LabelEntry: &aftpb.Afts_LabelEntry{ + NextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }}, + }}, + inDefaultName: "DEFAULT", + wantRIB: &RIB{ + defaultName: defaultName, + niRIB: map[string]*RIBHolder{ + "DEFAULT": { + name: "DEFAULT", + r: &aft.RIB{ + Afts: &aft.Afts{ + LabelEntry: map[aft.Afts_LabelEntry_Label_Union]*aft.Afts_LabelEntry{ + aft.UnionUint32(42): { + Label: aft.UnionUint32(42), + NextHopGroup: ygot.Uint64(42), + }, + }, + }, + }, + }, + }, + }, + }, { + desc: "multiple network instances", + inResponses: []*spb.GetResponse{{ + Entry: []*spb.AFTEntry{{ + NetworkInstance: "VRF-1", + Entry: &spb.AFTEntry_Ipv4{ + Ipv4: &aftpb.Afts_Ipv4EntryKey{ + Prefix: "1.1.1.1/32", + Ipv4Entry: &aftpb.Afts_Ipv4Entry{ + NextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }, { + NetworkInstance: defaultName, + Entry: &spb.AFTEntry_Ipv4{ + Ipv4: &aftpb.Afts_Ipv4EntryKey{ + Prefix: "2.2.2.2/32", + Ipv4Entry: &aftpb.Afts_Ipv4Entry{ + NextHopGroup: &wpb.UintValue{Value: 42}, + }, + }, + }, + }}, + }}, + inDefaultName: "DEFAULT", + wantRIB: &RIB{ + defaultName: defaultName, + niRIB: map[string]*RIBHolder{ + "DEFAULT": { + name: "DEFAULT", + r: &aft.RIB{ + Afts: &aft.Afts{ + Ipv4Entry: map[string]*aft.Afts_Ipv4Entry{ + "2.2.2.2/32": { + Prefix: ygot.String("2.2.2.2/32"), + NextHopGroup: ygot.Uint64(42), + }, + }, + }, + }, + }, + "VRF-1": { + name: "VRF-1", + r: &aft.RIB{ + Afts: &aft.Afts{ + Ipv4Entry: map[string]*aft.Afts_Ipv4Entry{ + "1.1.1.1/32": { + Prefix: ygot.String("1.1.1.1/32"), + NextHopGroup: ygot.Uint64(42), + }, + }, + }, + }, + }, + }, + }, + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + got, err := RIBFromGetResponses(tt.inDefaultName, tt.inResponses) + if (err != nil) != tt.wantErr { + t.Fatalf("RIBFromGetResponses(...): did not get expected error, got: %v, wantErr? %v", err, tt.wantErr) + } + + if diff := cmp.Diff(got, tt.wantRIB, + cmpopts.EquateEmpty(), cmp.AllowUnexported(RIB{}), + cmpopts.IgnoreFields(RIB{}, "nrMu", "pendMu", "ribCheck"), + cmp.AllowUnexported(RIBHolder{}), + cmpopts.IgnoreFields(RIBHolder{}, "mu", "refCounts", "checkFn"), + ); diff != "" { + t.Fatalf("RIBFromGetResponses(...): did not get expected RIB, diff(-got,+want):\n%s", diff) + } + }) + } +}