diff --git a/client/client.go b/client/client.go index 660039d8..bb837b9e 100644 --- a/client/client.go +++ b/client/client.go @@ -544,7 +544,7 @@ func (o *OpResult) String() string { } if v := o.OperationID; v != 0 { - buf.WriteString(fmt.Sprintf(" AFTOperation { ID: %d, Status: %s }", v, o.ProgrammingResult)) + buf.WriteString(fmt.Sprintf(" AFTOperation { ID: %d, Type: %s, Status: %s }", v, o.Details.Type, o.ProgrammingResult)) } if v := o.SessionParameters; v != nil { @@ -613,6 +613,12 @@ func (c *Client) StartSending() { c.qs.sendq = []*spb.ModifyRequest{} } +// StopSending toggles the client to stop sending messages to the server, meaning +// that entries that are enqueued will be stored until StartSending is called. +func (c *Client) StopSending() { + c.qs.sending.Store(false) +} + // handleModifyRequest performs any required post-processing after having sent a // ModifyRequest onto the gRPC channel to the server. Particularly, this ensures // that pending transactions are enqueued into the pending queue where they have diff --git a/compliance/compliance.go b/compliance/compliance.go index d0ebfb6f..35cff2a3 100644 --- a/compliance/compliance.go +++ b/compliance/compliance.go @@ -47,6 +47,8 @@ type Test struct { Description string // ShortName is a short description of the test for use in test output. ShortName string + // Reference is a unique reference to external data (e.g., test plans) used for the test. + Reference string } // TestSpec is a description of a test. @@ -79,11 +81,13 @@ var ( }, { In: Test{ Fn: AddIPv4EntryRIBACK, + Reference: "TE-2.1.1.1", ShortName: "Add IPv4 entry that can be programmed on the server - with RIB ACK", }, }, { In: Test{ Fn: AddIPv4EntryFIBACK, + Reference: "TE-2.1.1.2", ShortName: "Add IPv4 entry that can be programmed on the server - with FIB ACK", }, }, { @@ -101,6 +105,43 @@ var ( Fn: AddIPv4EntryRandom, ShortName: "Add IPv4 entries that are resolved by NHG and NH, in random order", }, + }, { + In: Test{ + Fn: AddIPv4ToMultipleNHsSingleRequest, + ShortName: "Add IPv4 entries that are resolved to a next-hop-group containing multiple next-hops (single ModifyRequest)", + Reference: "TE-2.1.2.1", + }, + }, { + In: Test{ + Fn: AddIPv4ToMultipleNHsMultipleRequests, + ShortName: "Add IPv4 entries that are resolved to a next-hop-group containing multiple next-hops (multiple ModifyRequests)", + Reference: "TE-2.1.2.2", + }, + }, { + In: Test{ + Fn: DeleteIPv4Entry, + ShortName: "Delete IPv4 entry within default network instance", + }, + }, { + In: Test{ + Fn: DeleteReferencedNHGFailure, + ShortName: "Delete NHG entry that is referenced - failure", + }, + }, { + In: Test{ + Fn: DeleteReferencedNHFailure, + ShortName: "Delete NH entry that is referenced - failure", + }, + }, { + In: Test{ + Fn: DeleteNextHopGroup, + ShortName: "Delete NHG entry successfully", + }, + }, { + In: Test{ + Fn: DeleteNextHop, + ShortName: "Delete NH entry successfully", + }, }} ) @@ -234,8 +275,6 @@ func addIPv4Internal(c *fluent.GRIBIClient, t testing.TB, wantACK fluent.Program WithProgrammingResult(wantACK). AsResult(), ) - - // TODO(robjs): add gNMI subscription using generated telemetry library. } // addIPv4Random adds an IPv4 Entry, shuffling the order of the entries, and @@ -357,3 +396,242 @@ func addNextHopGroupInternal(c *fluent.GRIBIClient, t testing.TB, wantACK fluent AsResult(), ) } + +// For the following tests, the base topology shown below is assumed. +// +// Topology: ________ +// | | +// -----port1----- | DUT |-----port2---- +// 192.0.2.0/31 | | 192.0.2.2/31 +// | | +// | |----port3----- +// | | 192.0.2.4/31 +// |________| +// +// -------------------1.0.0.0/8--------------> +// +// As the dataplane implementation is added, the input configuration +// within the test will cover the configuration of these ports, however, +// at this time the diagram above is illustrative for tracking the tests. + +// baseTopologyEntries creates the entries shown in the diagram above using +// separate ModifyRequests for each entry. +func baseTopologyEntries(c *fluent.GRIBIClient, t testing.TB) { + c.Modify().AddEntry(t, fluent.NextHopEntry().WithNetworkInstance(server.DefaultNetworkInstanceName).WithIndex(1).WithIPAddress("192.0.2.3")) + c.Modify().AddEntry(t, fluent.NextHopEntry().WithNetworkInstance(server.DefaultNetworkInstanceName).WithIndex(2).WithIPAddress("192.0.2.5")) + c.Modify().AddEntry(t, fluent.NextHopGroupEntry().WithNetworkInstance(server.DefaultNetworkInstanceName).WithID(1).AddNextHop(1, 1).AddNextHop(2, 1)) + c.Modify().AddEntry(t, fluent.IPv4Entry().WithPrefix("1.0.0.0/8").WithNetworkInstance(server.DefaultNetworkInstanceName).WithNextHopGroup(1)) +} + +// validateBaseEntries checks that the entries in the base topology are correctly +// installed. +func validateBaseTopologyEntries(res []*client.OpResult, t testing.TB) { + // Check for next-hops 1 and 2. + for _, nhopID := range []uint64{1, 2} { + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopOperation(nhopID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + chk.IgnoreOperationID(), + ) + } + + // Check for next-hop-group 1. + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopGroupOperation(1). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + chk.IgnoreOperationID(), + ) + + // Check for 1/8. + chk.HasResult(t, res, + fluent.OperationResult(). + WithIPv4Operation("1.0.0.0/8"). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + chk.IgnoreOperationID(), + ) +} + +// AddIPv4ToMultipleNHsSingleRequest creates an IPv4 entry which references a NHG containing +// 2 NHs within a single ModifyRequest, validating that they are installed in the FIB. +func AddIPv4ToMultipleNHsSingleRequest(c *fluent.GRIBIClient, t testing.TB) { + + ops := []func(){ + func() { + c.Modify().AddEntry(t, + fluent.NextHopEntry().WithNetworkInstance(server.DefaultNetworkInstanceName).WithIndex(1).WithIPAddress("192.0.2.3"), + fluent.NextHopEntry().WithNetworkInstance(server.DefaultNetworkInstanceName).WithIndex(2).WithIPAddress("192.0.2.5"), + fluent.NextHopGroupEntry().WithNetworkInstance(server.DefaultNetworkInstanceName).WithID(1).AddNextHop(1, 1).AddNextHop(2, 1), + fluent.IPv4Entry().WithPrefix("1.0.0.0/8").WithNetworkInstance(server.DefaultNetworkInstanceName).WithNextHopGroup(1)) + }, + } + + validateBaseTopologyEntries(doOps(c, t, ops, fluent.InstalledInFIB, false), t) +} + +// AddIPv4ToMultipleNHsMultipleRequests creates an IPv4 entry which references a NHG containing +// 2 NHs within multiple ModifyReqests, validating that they are installed in the FIB. +func AddIPv4ToMultipleNHsMultipleRequests(c *fluent.GRIBIClient, t testing.TB) { + + ops := []func(){ + func() { baseTopologyEntries(c, t) }, + } + validateBaseTopologyEntries(doOps(c, t, ops, fluent.InstalledInFIB, false), t) +} + +// DeleteIPv4Entry deletes an IPv4 entry from the server's RIB. +func DeleteIPv4Entry(c *fluent.GRIBIClient, t testing.TB) { + ops := []func(){ + func() { baseTopologyEntries(c, t) }, + func() { + c.Modify().DeleteEntry(t, fluent.IPv4Entry().WithPrefix("1.0.0.0/8").WithNetworkInstance(server.DefaultNetworkInstanceName)) + }, + } + res := doOps(c, t, ops, fluent.InstalledInFIB, false) + validateBaseTopologyEntries(res, t) + + chk.HasResult(t, res, + fluent.OperationResult(). + WithIPv4Operation("1.0.0.0/8"). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.InstalledInFIB). + AsResult(), + chk.IgnoreOperationID(), + ) +} + +// DeleteReferencedNHGFailure attempts to delete a NextHopGroup entry that is referenced +// from the RIB, and expects a failure. +func DeleteReferencedNHGFailure(c *fluent.GRIBIClient, t testing.TB) { + ops := []func(){ + func() { baseTopologyEntries(c, t) }, + func() { + c.Modify().DeleteEntry(t, fluent.NextHopGroupEntry().WithID(1).WithNetworkInstance(server.DefaultNetworkInstanceName)) + }, + } + res := doOps(c, t, ops, fluent.InstalledInFIB, false) + validateBaseTopologyEntries(res, t) + + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopGroupOperation(1). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.ProgrammingFailed). + AsResult(), + chk.IgnoreOperationID()) +} + +// DeleteReferencedNHFailure attempts to delete a NH entry that is referened from the RIB +// and expects a failure. +func DeleteReferencedNHFailure(c *fluent.GRIBIClient, t testing.TB) { + ops := []func(){ + func() { baseTopologyEntries(c, t) }, + func() { + c.Modify().DeleteEntry(t, fluent.NextHopEntry().WithIndex(1).WithNetworkInstance(server.DefaultNetworkInstanceName)) + }, + func() { + c.Modify().DeleteEntry(t, fluent.NextHopEntry().WithIndex(2).WithNetworkInstance(server.DefaultNetworkInstanceName)) + }, + } + res := doOps(c, t, ops, fluent.InstalledInFIB, false) + validateBaseTopologyEntries(res, t) + + for _, i := range []uint64{1, 2} { + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopOperation(i). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.ProgrammingFailed). + AsResult(), + chk.IgnoreOperationID()) + } +} + +// DeleteNextHopGroup attempts to delete a NHG entry that is not referenced and expects +// success. +func DeleteNextHopGroup(c *fluent.GRIBIClient, t testing.TB) { + ops := []func(){ + func() { baseTopologyEntries(c, t) }, + func() { + c.Modify().DeleteEntry(t, + fluent.IPv4Entry().WithPrefix("1.0.0.0/8").WithNetworkInstance(server.DefaultNetworkInstanceName), + fluent.NextHopGroupEntry().WithID(1).WithNetworkInstance(server.DefaultNetworkInstanceName), + ) + }, + } + + res := doOps(c, t, ops, fluent.InstalledInFIB, false) + validateBaseTopologyEntries(res, t) + + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopGroupOperation(1). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.InstalledInFIB). + AsResult(), + chk.IgnoreOperationID()) + + chk.HasResult(t, res, + fluent.OperationResult(). + WithIPv4Operation("1.0.0.0/8"). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.InstalledInFIB). + AsResult(), + chk.IgnoreOperationID()) +} + +// DeleteNextHop attempts to delete the NH entris within the base topology and expects +// success. +// +// TODO(robjs): When traffic and AFT validation is added, ensure that a partial delete +// scenario keeps traffic routed via the remaining NH. +func DeleteNextHop(c *fluent.GRIBIClient, t testing.TB) { + ops := []func(){ + func() { baseTopologyEntries(c, t) }, + func() { + c.Modify().DeleteEntry(t, + fluent.IPv4Entry().WithPrefix("1.0.0.0/8").WithNetworkInstance(server.DefaultNetworkInstanceName), + fluent.NextHopGroupEntry().WithID(1).WithNetworkInstance(server.DefaultNetworkInstanceName), + fluent.NextHopEntry().WithIndex(1).WithNetworkInstance(server.DefaultNetworkInstanceName), + fluent.NextHopEntry().WithIndex(2).WithNetworkInstance(server.DefaultNetworkInstanceName), + ) + }, + } + + res := doOps(c, t, ops, fluent.InstalledInFIB, false) + validateBaseTopologyEntries(res, t) + + for _, i := range []uint64{1, 2} { + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopOperation(i). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.InstalledInFIB). + AsResult(), + chk.IgnoreOperationID()) + + } + + chk.HasResult(t, res, + fluent.OperationResult(). + WithNextHopGroupOperation(1). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.InstalledInFIB). + AsResult(), + chk.IgnoreOperationID()) + + chk.HasResult(t, res, + fluent.OperationResult(). + WithIPv4Operation("1.0.0.0/8"). + WithOperationType(constants.Delete). + WithProgrammingResult(fluent.InstalledInFIB). + AsResult(), + chk.IgnoreOperationID()) +} diff --git a/fluent/fluent.go b/fluent/fluent.go index e2eae68e..3a5083be 100644 --- a/fluent/fluent.go +++ b/fluent/fluent.go @@ -207,9 +207,12 @@ func (g *GRIBIClient) Start(ctx context.Context, t testing.TB) { g.ctx = ctx } +// Stop specifies that the gRIBI client should stop sending operations, +// and subsequently disconnect from the server. func (g *GRIBIClient) Stop(t testing.TB) { + g.c.StopSending() if err := g.c.Close(); err != nil { - t.Fatalf("cannot disconnect from client, %v", err) + t.Fatalf("cannot disconnect from server, %v", err) } } @@ -445,6 +448,13 @@ func (i *ipv4Entry) WithNextHopGroup(u uint64) *ipv4Entry { return i } +// WithNextHopGroupNetworkInstance specifies the network-instance within which +// the next-hop-group for the IPv4 entry should be resolved. +func (i *ipv4Entry) WithNextHopGroupNetworkInstance(n string) *ipv4Entry { + i.pb.Ipv4Entry.NextHopGroupNetworkInstance = &wpb.StringValue{Value: n} + return i +} + // opproto implements the gRIBIEntry interface, returning a gRIBI AFTOperation. ID // and ElectionID are explicitly not populated such that they can be populated by // the function (e.g., AddEntry) to which they are an argument. diff --git a/gnmit/gnmit.go b/gnmit/gnmit.go index 91c4b09e..e8bb9c1b 100644 --- a/gnmit/gnmit.go +++ b/gnmit/gnmit.go @@ -51,6 +51,12 @@ func periodic(period time.Duration, fn func()) { } } +// GNMIServer implements the gNMI server interface. +type GNMIServer struct { + // The subscribe Server implements only Subscribe for gNMI. + *subscribe.Server +} + // New returns a new collector that listens on the specified addr (in the form host:port), // supporting a single downstream target named hostname. sendMeta controls whether the // metadata *other* than meta/sync and meta/connected is sent by the collector. @@ -91,7 +97,12 @@ func New(ctx context.Context, addr string, hostname string, sendMeta bool, opts if err != nil { return nil, "", fmt.Errorf("could not instantiate gNMI server: %v", err) } - gpb.RegisterGNMIServer(srv, subscribeSrv) + + gnmiserver := &GNMIServer{ + Server: subscribeSrv, // use the 'subscribe' implementation. + } + + gpb.RegisterGNMIServer(srv, gnmiserver) // Forward streaming updates to clients. c.cache.SetClient(subscribeSrv.Update) // Register listening port and start serving. diff --git a/go.sum b/go.sum index b85a8c85..4081ad08 100644 --- a/go.sum +++ b/go.sum @@ -56,7 +56,6 @@ github.com/openconfig/gnmi v0.0.0-20200508230933-d19cebf5e7be/go.mod h1:M/EcuapN github.com/openconfig/gnmi v0.0.0-20210527163611-d3a3e30199da h1:Gaj4Reje4wKdliTXaXDE7ginHeVzDbcUTszUx6xpQeE= github.com/openconfig/gnmi v0.0.0-20210527163611-d3a3e30199da/go.mod h1:H/20NXlnWbCPFC593nxpiKJ+OU//7mW7s7Qk7uVdg3Q= github.com/openconfig/goyang v0.0.0-20200115183954-d0a48929f0ea/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU= -github.com/openconfig/goyang v0.2.2 h1:J8hlJk1GSHrcr9vVI7dTvsThsKihWcNXRjWOkjRK0Cw= github.com/openconfig/goyang v0.2.2/go.mod h1:vX61x01Q46AzbZUzG617vWqh/cB+aisc+RrNkXRd3W8= github.com/openconfig/goyang v0.2.5 h1:ZvV+5cF5thPFun1H6/Itt/Jbwb/bKmjS0o8imN+7ddw= github.com/openconfig/goyang v0.2.5/go.mod h1:vX61x01Q46AzbZUzG617vWqh/cB+aisc+RrNkXRd3W8= @@ -66,8 +65,6 @@ github.com/openconfig/grpctunnel v0.0.0-20210610163803-fde4a9dc048d h1:zrs4U92QE github.com/openconfig/grpctunnel v0.0.0-20210610163803-fde4a9dc048d/go.mod h1:x9tAZ4EwqCQ0jI8D6S8Yhw9Z0ee7/BxWQX0k0Uib5Q8= github.com/openconfig/ygot v0.6.0/go.mod h1:o30svNf7O0xK+R35tlx95odkDmZWS9JyWWQSmIhqwAs= github.com/openconfig/ygot v0.10.4/go.mod h1:oCQNdXnv7dWc8scTDgoFkauv1wwplJn5HspHcjlxSAQ= -github.com/openconfig/ygot v0.10.15 h1:150MEjCnCtd6x8WEjLSgsmdC/Emzd5Pp1sIfZAX9QKc= -github.com/openconfig/ygot v0.10.15/go.mod h1:NDGFcX73PnipISpF8yuDpwrsOAl6vqqK4mNPvscWzXA= github.com/openconfig/ygot v0.11.0 h1:CAi4Uk96tdonyoWP7uMFqccpA0G5OUCgBpPGBpYre4E= github.com/openconfig/ygot v0.11.0/go.mod h1:5q5fz1SDPGUwMyzbm8Ns2Krul+32euNSU89ZmrGrSK8= github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= diff --git a/rib/rib.go b/rib/rib.go index 095d7450..e27c9a26 100644 --- a/rib/rib.go +++ b/rib/rib.go @@ -400,10 +400,10 @@ func (r *RIB) addEntryInternal(ni string, op *spb.AFTOperation, oks, fails *[]*O refdNHGID = t.Ipv4.GetIpv4Entry().GetNextHopGroup().GetValue() v4Prefix = t.Ipv4.GetPrefix() - log.V(2).Infof("adding IPv4 prefix %s", t.Ipv4.GetPrefix()) + log.V(2).Infof("[op %d] attempting to add IPv4 prefix %s", op.GetId(), t.Ipv4.GetPrefix()) installed, replaced, err = niR.AddIPv4(t.Ipv4, explicitReplace) case *spb.AFTOperation_NextHop: - log.V(2).Infof("adding NH Index %d", t.NextHop.GetIndex()) + log.V(2).Infof("[op %d] attempting to add NH Index %d", op.GetId(), t.NextHop.GetIndex()) installed, replaced, err = niR.AddNextHop(t.NextHop, explicitReplace) case *spb.AFTOperation_NextHopGroup: nhgID = t.NextHopGroup.GetId() @@ -412,7 +412,7 @@ func (r *RIB) addEntryInternal(ni string, op *spb.AFTOperation, oks, fails *[]*O refdNextHops = append(refdNextHops, v.GetIndex()) } - log.V(2).Infof("adding NHG ID %d", t.NextHopGroup.GetId()) + log.V(2).Infof("[op %d] attempting to add NHG ID %d", op.GetId(), t.NextHopGroup.GetId()) installed, replaced, err = niR.AddNextHopGroup(t.NextHopGroup, explicitReplace) default: return status.Newf(codes.Unimplemented, "unsupported AFT operation type %T", t).Err() @@ -450,7 +450,9 @@ func (r *RIB) addEntryInternal(ni string, op *spb.AFTOperation, oks, fails *[]*O // we don't retry if it was somewhere further up the stack. installStack[op.GetId()] = true log.V(2).Infof("operation %d installed in RIB successfully", op.GetId()) + r.rmPending(op.GetId()) + *oks = append(*oks, &OpResult{ ID: op.GetId(), Op: op, @@ -464,7 +466,7 @@ func (r *RIB) addEntryInternal(ni string, op *spb.AFTOperation, oks, fails *[]*O } // we may now have made some other pending entry be possible to install, - // so try them all out! + // so try them all out. for _, e := range r.getPending() { err := r.addEntryInternal(e.ni, e.op, oks, fails, installStack) if err != nil { diff --git a/server/server.go b/server/server.go index a261e831..df51186e 100644 --- a/server/server.go +++ b/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/openconfig/gribigo/rib" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/prototext" "lukechampine.com/uint128" spb "github.com/openconfig/gribi/v1/proto/service" @@ -657,7 +658,6 @@ func (s *Server) doModify(cid string, ops []*spb.AFTOperation, resCh chan *spb.M elec.clientLatest = cs.lastElecID elec.client = cid - var wg sync.WaitGroup for _, o := range ops { ni := o.GetNetworkInstance() if ni == "" { @@ -677,19 +677,26 @@ func (s *Server) doModify(cid string, ops []*spb.AFTOperation, resCh chan *spb.M return } - wg.Add(1) - go func(op *spb.AFTOperation) { - res, err := modifyEntry(s.masterRIB, ni, op, cs.params.FIBAck, elec) - switch { - case err != nil: - errCh <- err - default: - resCh <- res - } - wg.Done() - }(o) + // We do not try and modify entries within the operation in parallel + // with each other since this may cause us to duplicate ACK on particular + // operations - for example, if there are two next-hops that are within a + // single next-hop-group, then both of them will cause the NHG to be + // installed in the RIB. If we do this then we might end up ACKing + // one twice if there was >1 different entry that mde a NHG resolvable. + // For a SINGLE_PRIMARY client serialising into a single Modify channel + // ensures that we do not end up with this occuring - but going forward + // for ALL_PRIMARY this situation will need to handled likely by creating + // some form of lock on each transaction as it is attempted, or building + // a more intelligent RIB structure to track missing dependencies. + res, err := modifyEntry(s.masterRIB, ni, o, cs.params.FIBAck, elec) + switch { + case err != nil: + errCh <- err + default: + resCh <- res + } } - wg.Wait() + } // electionDetails provides a summary of a single election from the perspective of one client. @@ -755,6 +762,7 @@ func modifyEntry(r *rib.RIB, ni string, op *spb.AFTOperation, fibACK bool, elect // AddEntry handles replaces, since an ADD can be an explicit replace. It checks // whether the entry was an explicit replace from the op, and if so errors if the // entry does not already exist. + log.V(2).Infof("calling AddEntry for operation ID %d", op.GetId()) oks, faileds, err = r.AddEntry(ni, op) case spb.AFTOperation_DELETE: oks, faileds, err = r.DeleteEntry(ni, op) @@ -772,6 +780,7 @@ func modifyEntry(r *rib.RIB, ni string, op *spb.AFTOperation, fibACK bool, elect ) } for _, ok := range oks { + log.V(2).Infof("received OK for %d in operation %s", ok.ID, prototext.Format(op)) results = append(results, &spb.AFTResult{ Id: ok.ID, Status: okACK, diff --git a/server/server_test.go b/server/server_test.go index 2e26334b..06f9bbbd 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -846,6 +846,107 @@ func TestDoModify(t *testing.T) { }}, }, }}, + }, { + desc: "ipv4 to one next-hop group containing two next-hops", + inServer: func() *Server { + s := New() + s.cs["testclient"] = &clientState{ + params: &clientParams{ + Persist: true, + ExpectElecID: true, + FIBAck: true, + }, + lastElecID: &spb.Uint128{High: 42, Low: 42}, + } + s.curElecID = &spb.Uint128{High: 42, Low: 42} + s.curMaster = "testclient" + return s + }(), + inCID: "testclient", + inOps: []*spb.AFTOperation{{ + Id: 1, + NetworkInstance: "", + Op: spb.AFTOperation_ADD, + ElectionId: &spb.Uint128{High: 42, Low: 42}, + Entry: &spb.AFTOperation_NextHop{ + NextHop: &aftpb.Afts_NextHopKey{ + Index: 1, + NextHop: &aftpb.Afts_NextHop{}, + }, + }, + }, { + Id: 2, + NetworkInstance: "", + Op: spb.AFTOperation_ADD, + ElectionId: &spb.Uint128{High: 42, Low: 42}, + Entry: &spb.AFTOperation_NextHop{ + NextHop: &aftpb.Afts_NextHopKey{ + Index: 2, + NextHop: &aftpb.Afts_NextHop{}, + }, + }, + }, { + Id: 3, + NetworkInstance: "", + Op: spb.AFTOperation_ADD, + ElectionId: &spb.Uint128{High: 42, Low: 42}, + Entry: &spb.AFTOperation_NextHopGroup{ + NextHopGroup: &aftpb.Afts_NextHopGroupKey{ + Id: 1, + NextHopGroup: &aftpb.Afts_NextHopGroup{ + NextHop: []*aftpb.Afts_NextHopGroup_NextHopKey{{ + Index: 1, + NextHop: &aftpb.Afts_NextHopGroup_NextHop{}, + }, { + Index: 2, + NextHop: &aftpb.Afts_NextHopGroup_NextHop{}, + }}, + }, + }, + }, + }, { + Id: 4, + NetworkInstance: "", + Op: spb.AFTOperation_ADD, + ElectionId: &spb.Uint128{High: 42, Low: 42}, + Entry: &spb.AFTOperation_Ipv4{ + Ipv4: &aftpb.Afts_Ipv4EntryKey{ + Prefix: "1.0.0.0/8", + Ipv4Entry: &aftpb.Afts_Ipv4Entry{ + NextHopGroup: &wpb.UintValue{Value: 1}, + }, + }, + }, + }}, + wantMsg: []*expectedMsg{{ + result: &spb.ModifyResponse{ + Result: []*spb.AFTResult{{ + Id: 1, + Status: spb.AFTResult_FIB_PROGRAMMED, + }}, + }, + }, { + result: &spb.ModifyResponse{ + Result: []*spb.AFTResult{{ + Id: 2, + Status: spb.AFTResult_FIB_PROGRAMMED, + }}, + }, + }, { + result: &spb.ModifyResponse{ + Result: []*spb.AFTResult{{ + Id: 3, + Status: spb.AFTResult_FIB_PROGRAMMED, + }}, + }, + }, { + result: &spb.ModifyResponse{ + Result: []*spb.AFTResult{{ + Id: 4, + Status: spb.AFTResult_FIB_PROGRAMMED, + }}, + }, + }}, }} type recvMsg struct { @@ -888,6 +989,10 @@ func TestDoModify(t *testing.T) { sort.Slice(got, lessFn) sort.Slice(tt.wantMsg, lessFn) + if len(got) != len(tt.wantMsg) { + t.Fatalf("did not get expected number of messages, got: %d, want: %d", len(got), len(tt.wantMsg)) + } + for i := 0; i < len(tt.wantMsg); i++ { wantMsg := tt.wantMsg[i] gotMsg := got[i]