From d37944819c43ae5b8b04b97e87a6da07dcff7c48 Mon Sep 17 00:00:00 2001 From: moharedd Date: Sat, 13 Jun 2026 03:11:11 +0000 Subject: [PATCH 1/3] Adding AFT-6.4: AFT Prefix Filtering Dynamic Updates Test --- .../afts_prefix_filtering_dynamic_test.go | 564 ++++++++++++++++++ .../metadata.textproto | 10 + internal/cfgplugins/policyforwarding.go | 78 +++ internal/deviations/deviations.go | 5 + internal/telemetry/aftcache/aft_cache.go | 130 +++- proto/metadata.proto | 4 + proto/metadata_go_proto/metadata.pb.go | 32 +- 7 files changed, 811 insertions(+), 12 deletions(-) create mode 100644 feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go diff --git a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go new file mode 100644 index 00000000000..38e29b3de50 --- /dev/null +++ b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go @@ -0,0 +1,564 @@ +// Copyright 2026 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 + +// Package aftsprefixfilteringdynamic implements AFT-6.4: +// AFT Prefix Filtering Dynamic Updates. +package afts_prefix_filtering_dynamic_test + +import ( + "context" + "flag" + "fmt" + "sync" + "testing" + "time" + + "github.com/open-traffic-generator/snappi/gosnappi" + "github.com/openconfig/featureprofiles/internal/attrs" + "github.com/openconfig/featureprofiles/internal/cfgplugins" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/telemetry/aftcache" + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygot/ygot" +) + +const ( + vrfName = "VRF-A" + v4PfxSet = "PREFIX-SET-A" + v6PfxSet = "PREFIX-SET-B" + v4Policy = "POLICY-PREFIX-SET-A" + v6Policy = "POLICY-PREFIX-SET-B" + matchAllPolicy = "POLICY-MATCH-ALL" + subscriptionWait = 3 * time.Minute + prefixAft1V4 = "198.51.100.0/24" + prefixAft2V4 = "203.0.113.0/28" + prefixAft3V4 = "192.0.2.0/24" + prefixAft1V6 = "2001:db8:2::/64" + prefixAft2V6 = "2001:db8:2::1/128" + prefixAft3V6 = "2001:db8:2::2/128" + vrfV4Pfx = "100.64.1.0/24" + vrfV6Pfx = "2001:db8:3::/64" + maskRange = "exact" +) + +var ( + dutPort1 = attrs.Attributes{ + Desc: "DUT to ATE Port 1", + MAC: "02:00:02:02:02:02", + IPv4: "192.0.2.1", + IPv4Len: 30, + IPv6: "2001:db8:0:1::1", + IPv6Len: 64, + } + atePort1 = attrs.Attributes{ + Name: "atePort1", + Desc: "ATE to DUT Port 1", + MAC: "02:00:02:01:01:01", + IPv4: "192.0.2.2", + IPv4Len: 30, + IPv6: "2001:db8:0:1::2", + IPv6Len: 64, + } + dutPort2 = attrs.Attributes{ + Desc: "DUT to ATE Port 2", + MAC: "02:00:04:02:02:02", + IPv4: "192.0.3.1", + IPv4Len: 30, + IPv6: "2001:db8:0:2::1", + IPv6Len: 64, + } + atePort2 = attrs.Attributes{ + Name: "atePort2", + Desc: "ATE to DUT Port 2", + MAC: "02:00:04:01:01:01", + IPv4: "192.0.3.2", + IPv4Len: 30, + IPv6: "2001:db8:0:2::2", + IPv6Len: 64, + } + + defaultIPv4Prefixes = []string{ + "198.51.100.0/24", + "203.0.113.0/28", + "100.64.0.0/24", + } + + policyIPv4Prefixes = []string{ + "198.51.100.0/24", + "203.0.113.0/28", + "198.51.100.1/32", + "192.0.2.0/24", + } + + defaultIPv6Prefixes = []string{ + "2001:db8:2::/64", + "2001:db8:2::1/128", + "2001:db8:2::2/128", + } + + policyIPv6Prefixes = []string{ + "2001:db8:2::/64", + "2001:db8:2::1/128", + } + + vrfV4Prefixes = []string{ + "198.51.100.0/24", + "100.64.1.0/24", + "203.0.113.128/28", + } + vrfV6Prefixes = []string{ + "2001:db8:2::/64", + "2001:db8:2::1/128", + "2001:db8:2::2/128", + } + debugNotifications = flag.Bool("debug_notifications", true, "Enable full AFT notification recording") +) + +type dynamicUpdateTestParams struct { + testID string + prefixSet string + prefix1 string + prefix2 string + prefix3 string + nhIP string + maskRange string + policyName string + indx string +} + +// TestMain runs featureprofile tests. +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +// TestAFTPrefixFilteringDynamicUpdates implements AFT-6.4. +func TestAFTPrefixFilteringDynamicUpdates(t *testing.T) { + dut := ondatra.DUT(t, "dut") + ate := ondatra.ATE(t, "ate") + batch := configureDUT(t, dut) + configurePolicies(t, dut, batch) + configureStaticRoutes(t, dut, batch, defaultIPv4Prefixes, vrfV4Prefixes, atePort1.IPv4, atePort2.IPv4, 100) + configureStaticRoutes(t, dut, batch, defaultIPv6Prefixes, vrfV6Prefixes, atePort1.IPv6, atePort2.IPv6, 200) + topo, interfaceNamesList := configureATE(t, ate) + ate.OTG().PushConfig(t, topo) + ate.OTG().StartProtocols(t) + cfgplugins.IsIPv4InterfaceARPresolved(t, ate, cfgplugins.AddressFamilyParams{InterfaceNames: interfaceNamesList}) + cfgplugins.IsIPv6InterfaceARPresolved(t, ate, cfgplugins.AddressFamilyParams{InterfaceNames: interfaceNamesList}) + + tests := []struct { + name string + test func(t *testing.T, dut *ondatra.DUTDevice) + }{ + { + name: "AFT-6.4.1-DynamicIPv4PrefixSetUpdates", + test: validateIPv4DynamicUpdates, + }, + { + name: "AFT-6.4.2-DynamicIPv6PrefixSetUpdates", + test: validateIPv6DynamicUpdates, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.test(t, dut) + }) + } +} + +// configureDUT configures the DUT with the necessary VRF, interfaces, BGP, and redistribution policies. +func configureDUT(t *testing.T, dut *ondatra.DUTDevice) *gnmi.SetBatch { + t.Helper() + batch := &gnmi.SetBatch{} + fptest.ConfigureDefaultNetworkInstance(t, dut) + configureHardwareInit(t, dut) + p1 := dut.Port(t, "port1") + p2 := dut.Port(t, "port2") + nonDefaultNI := cfgplugins.ConfigureNetworkInstance(t, dut, vrfName, false) + configureDUTInterface(t, dut, batch, &dutPort1, p1) + configureDUTInterface(t, dut, batch, &dutPort2, p2) + cfgplugins.UpdateNetworkInstanceOnDut(t, dut, vrfName, nonDefaultNI) + configureDUTPort(t, dut, batch, &dutPort2, p2, vrfName) + batch.Set(t, dut) + return batch +} + +// configureDUTInterface configure interfaces on DUT. +func configureDUTInterface(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, attrs *attrs.Attributes, p *ondatra.Port) { + t.Helper() + d := gnmi.OC() + i := attrs.NewOCInterface(p.Name(), dut) + i.Description = ygot.String(attrs.Desc) + i.Type = oc.IETFInterfaces_InterfaceType_ethernetCsmacd + if deviations.InterfaceEnabled(dut) { + i.Enabled = ygot.Bool(true) + } + + i.GetOrCreateEthernet() + i4 := i.GetOrCreateSubinterface(0).GetOrCreateIpv4() + i4.Enabled = ygot.Bool(true) + av4 := i4.GetOrCreateAddress(attrs.IPv4) + av4.PrefixLength = ygot.Uint8(attrs.IPv4Len) + + i6 := i.GetOrCreateSubinterface(0).GetOrCreateIpv6() + i6.Enabled = ygot.Bool(true) + av6 := i6.GetOrCreateAddress(attrs.IPv6) + av6.PrefixLength = ygot.Uint8(attrs.IPv6Len) + + gnmi.BatchUpdate(batch, d.Interface(p.Name()).Config(), i) +} + +// configureDUTPort configure DUT ports. +func configureDUTPort(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, attrs *attrs.Attributes, p *ondatra.Port, niName string) { + t.Helper() + d := gnmi.OC() + cfgplugins.AssignToNetworkInstance(t, dut, p.Name(), niName, 0) + i := attrs.NewOCInterface(p.Name(), dut) + gnmi.BatchUpdate(batch, d.Interface(p.Name()).Config(), i) +} + +// configureHardwareInit sets up the initial hardware configuration on the DUT. It pushes hardware initialization configs for VRF Selection Extended feature and Policy Forwarding feature. +func configureHardwareInit(t *testing.T, dut *ondatra.DUTDevice) { + t.Helper() + features := []cfgplugins.FeatureType{ + cfgplugins.FeatureVrfSelectionExtended, + cfgplugins.FeaturePolicyForwarding, + } + for _, feature := range features { + hardwareInitCfg := cfgplugins.NewDUTHardwareInit(t, dut, feature) + if hardwareInitCfg != "" { + cfgplugins.PushDUTHardwareInitConfig(t, dut, hardwareInitCfg) + } + } +} + +// configureATE configures the ATE ports and BGP neighbor. +func configureATE(t *testing.T, ate *ondatra.ATEDevice) (gosnappi.Config, []string) { + t.Helper() + var interfaceNamesList []string + topo := gosnappi.NewConfig() + p1 := ate.Port(t, "port1") + p2 := ate.Port(t, "port2") + + atePort1.AddToOTG(topo, p1, &dutPort1) + atePort2.AddToOTG(topo, p2, &dutPort2) + // Collect interface/device names + for _, dev := range topo.Devices().Items() { + interfaceNamesList = append(interfaceNamesList, dev.Name()) + } + return topo, interfaceNamesList +} + +// validateIPv4DynamicUpdates validates: +// +// 1. Initial filtered subscription. +// 2. Add matching IPv4 route -> visible. +// 3. Add non-matching IPv4 route -> not visible. +// 4. Delete matching route -> removed. +// 5. Dynamic policy update -> newly matched route becomes visible. +func validateIPv4DynamicUpdates(t *testing.T, dut *ondatra.DUTDevice) { + t.Helper() + validateDynamicUpdates(t, dut, + dynamicUpdateTestParams{ + testID: "AFT-6.4.1", + prefixSet: v4PfxSet, + prefix1: prefixAft1V4, + prefix2: prefixAft2V4, + prefix3: prefixAft3V4, + nhIP: atePort1.IPv4, + maskRange: maskRange, + policyName: v4Policy, + indx: "5001", + }, + ) +} + +// validateIPv6DynamicUpdates validates: +// +// 1. Initial filtered IPv6 subscription. +// 2. Add matching IPv6 route. +// 3. Add non-matching IPv6 route. +// 4. Delete matching IPv6 route. +// 5. Dynamic policy update. +func validateIPv6DynamicUpdates(t *testing.T, dut *ondatra.DUTDevice) { + t.Helper() + validateDynamicUpdates(t, dut, + dynamicUpdateTestParams{ + testID: "AFT-6.4.2", + prefixSet: v6PfxSet, + prefix1: prefixAft1V6, + prefix2: prefixAft2V6, + prefix3: prefixAft3V6, + nhIP: atePort1.IPv6, + maskRange: maskRange, + policyName: v6Policy, + indx: "6001", + }, + ) +} + +// validateDynamicUpdates validates dynamic AFT prefix filtering behavior for both IPv4 and IPv6 route policies. +func validateDynamicUpdates(t *testing.T, dut *ondatra.DUTDevice, pArgs dynamicUpdateTestParams) { + t.Helper() + ctx := context.Background() + configureGlobalFilterPolicies(t, dut, pArgs.policyName, "", deviations.DefaultNetworkInstance(dut)) + + wantPrefixes := map[string]bool{ + pArgs.prefix1: true, + pArgs.prefix2: true, + } + + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx) + if err != nil { + t.Fatalf("Failed to dial GNMI: %v", err) + } + + // ------------------------------------------------------------ + // Initial Sync + // ------------------------------------------------------------ + + t.Logf("%s - Initial Synchronization", pArgs.testID) + + initialCollector := newCollector(ctx, t, dut, gnmiClient) + + runCollector(ctx, t, initialCollector, aftcache.InitialSyncStoppingCondition(t, dut, wantPrefixes, nil, nil)) + aft, err := initialCollector.ToAFT(t, dut) + if err != nil { + t.Fatalf("ToAFT failed: %v", err) + } + + verifyPrefixesPresent(t, aft, []string{pArgs.prefix1, pArgs.prefix2}) + + // ------------------------------------------------------------ + // AFT-6.4.X.1 Add Prefix + // ------------------------------------------------------------ + + t.Logf("%s.1 - Addition of Prefix to Active Set", pArgs.testID) + + addCollector := newCollector(ctx, t, dut, gnmiClient) + mustAddSingleStaticRoute(t, dut, deviations.DefaultNetworkInstance(dut), pArgs.prefix3, pArgs.indx, pArgs.nhIP) + runCollector(ctx, t, addCollector, aftcache.InitialSyncStoppingCondition(t, dut, map[string]bool{pArgs.prefix3: true}, nil, nil)) + addAFT, err := addCollector.ToAFT(t, dut) + if err != nil { + t.Fatalf("ToAFT failed: %v", err) + } + verifyPrefixesPresent(t, addAFT, []string{pArgs.prefix3}) + // Wait until notification received or timeout + runCollector(ctx, t, addCollector, aftcache.WaitForUpdateNotification(t, aftcache.NotificationExpectation{AddPrefix: pArgs.prefix3})) + + // ------------------------------------------------------------ + // AFT-6.4.X.2 Delete Prefix + // ------------------------------------------------------------ + + t.Logf("%s.2 - Deletion of Prefix from Active Set", pArgs.testID) + + deleteCollector := newCollector(ctx, t, dut, gnmiClient) + removePrefixFromPrefixSet(t, dut, pArgs.prefixSet, pArgs.prefix1, pArgs.maskRange) + runCollector(ctx, t, deleteCollector, aftcache.InitialSyncStoppingCondition(t, dut, map[string]bool{pArgs.prefix1: true}, nil, nil)) + delaft, delerr := deleteCollector.ToAFT(t, dut) + if delerr != nil { + t.Fatalf("ToAFT failed: %v", delerr) + } + verifyPrefixRemovedFromPrefixSet(t, dut, pArgs.prefixSet, pArgs.prefix1, pArgs.maskRange) + verifyPrefixesAbsent(t, delaft, []string{pArgs.prefix1}) + // Wait until notification received or timeout + runCollector(ctx, t, deleteCollector, aftcache.WaitForDeleteNotification(t, aftcache.NotificationExpectation{DeletePrefix: pArgs.prefix1})) + + // ------------------------------------------------------------ + // AFT-6.4.X.3 Atomic Add/Delete + // ------------------------------------------------------------ + + t.Logf("%s.3 - Simultaneous Addition and Deletion", pArgs.testID) + + swapCollector := newCollector(ctx, t, dut, gnmiClient) + atomicPrefixSetSwap(t, dut, pArgs.prefixSet, pArgs.prefix1, pArgs.prefix2, pArgs.maskRange) + verifyPrefixRemovedFromPrefixSet(t, dut, pArgs.prefixSet, pArgs.prefix2, pArgs.maskRange) + runCollector(ctx, t, swapCollector, aftcache.InitialSyncStoppingCondition(t, dut, map[string]bool{pArgs.prefix3: true}, nil, nil)) + swapaft, swaperr := swapCollector.ToAFT(t, dut) + if swaperr != nil { + t.Fatalf("ToAFT failed: %v", swaperr) + } + verifyPrefixesPresent(t, swapaft, []string{pArgs.prefix1}) + verifyPrefixesAbsent(t, swapaft, []string{pArgs.prefix2}) + // Wait until notification received or timeout + runCollector(ctx, t, swapCollector, aftcache.WaitForUpdateDeleteNotification(t, aftcache.NotificationExpectation{AddPrefix: pArgs.prefix1, DeletePrefix: pArgs.prefix2})) +} + +// newCollector creates and returns a new AFT stream session. If debug_notifications is enabled, all received gNMI notifications are recorded in memory for later inspection and troubleshooting. +func newCollector(ctx context.Context, t *testing.T, dut *ondatra.DUTDevice, client gpb.GNMIClient) *aftcache.AFTStreamSession { + t.Helper() + c := aftcache.NewAFTStreamSession(ctx, t, client, dut) + if *debugNotifications { + c.WithDebug() + t.Log("DEBUG MODE ENABLED: Recording all gNMI notifications to memory.") + } + return c +} + +// runCollector starts the AFT stream collector and blocks until the supplied stopping condition is satisfied or the collector times out. +func runCollector(ctx context.Context, t *testing.T, collector *aftcache.AFTStreamSession, stop aftcache.PeriodicHook) { + t.Helper() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + collector.ListenUntil(ctx, t, subscriptionWait, stop) + }() + wg.Wait() +} + +// removePrefixFromPrefixSet removes the specified prefix entry from the given routing-policy prefix-set on the DUT. +func removePrefixFromPrefixSet(t *testing.T, dut *ondatra.DUTDevice, prefixSetName, prefix, maskRange string) { + t.Helper() + batch := &gnmi.SetBatch{} + gnmi.BatchDelete(batch, gnmi.OC().RoutingPolicy().DefinedSets().PrefixSet(prefixSetName).Prefix(prefix, maskRange).Config()) + batch.Set(t, dut) +} + +// verifyPrefixRemovedFromPrefixSet verifies that the specified prefix no longer exists in the given prefix-set configuration. +func verifyPrefixRemovedFromPrefixSet(t *testing.T, dut *ondatra.DUTDevice, prefixSetName, prefix, maskRange string) { + t.Helper() + path := gnmi.OC().RoutingPolicy().DefinedSets().PrefixSet(prefixSetName).Prefix(prefix, maskRange).Config() + + if val, present := gnmi.Lookup(t, dut, path).Val(); present { + t.Fatalf("Prefix %s mask-range %s still exists in prefix-set %s: %+v", prefix, maskRange, prefixSetName, val) + } + t.Logf("Verified prefix %s mask-range %s removed from prefix-set %s", prefix, maskRange, prefixSetName) +} + +// atomicPrefixSetSwap atomically adds one prefix and removes another from the specified prefix-set using a single gNMI Set transaction. +func atomicPrefixSetSwap(t *testing.T, dut *ondatra.DUTDevice, prefixName, addPrefixVal, delPrefixVal, prefixMode string) { + t.Helper() + batch := &gnmi.SetBatch{} + addPath := gnmi.OC().RoutingPolicy().DefinedSets().PrefixSet(prefixName).Prefix(addPrefixVal, prefixMode) + addEntry := &oc.RoutingPolicy_DefinedSets_PrefixSet_Prefix{ + IpPrefix: ygot.String(addPrefixVal), + MasklengthRange: ygot.String(prefixMode), + } + gnmi.BatchReplace(batch, addPath.Config(), addEntry) + delPath := gnmi.OC().RoutingPolicy().DefinedSets().PrefixSet(prefixName).Prefix(delPrefixVal, prefixMode) + gnmi.BatchDelete(batch, delPath.Config()) + + batch.Set(t, dut) +} + +// configureGlobalFilterPolicies configures AFT global-filter policies for the specified network-instance. +func configureGlobalFilterPolicies(t *testing.T, dut *ondatra.DUTDevice, ipv4Policy, ipv6Policy, vrfName string) { + t.Helper() + if deviations.AftsGlobalFilterPolicyOCUnsupported(dut) { + switch dut.Vendor() { + case ondatra.ARISTA: + t.Log("Skipping AFT global-filter attachment: unsupported on EOS") + } + } else { + // TODO: Enable the following code once OC supports AFTs global filter configuration. + // root := &oc.Root{} + // ni := root.GetOrCreateNetworkInstance(deviations.DefaultNetworkInstance(dut)) + // afts := ni.GetOrCreateAfts() + // gf := afts.GetOrCreateGlobalFilter() + // gf.Ipv4Policy = ygot.String(ipv4Policy) + // gf.Ipv6Policy = ygot.String(ipv6Policy) + // gnmi.Replace(t, dut, gnmi.OC().NetworkInstance(deviations.DefaultNetworkInstance(dut)).Afts().GlobalFilter().Config(), gf) + } +} + +// configurePolicies configures routing policies. +func configurePolicies(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch) { + t.Helper() + root := &oc.Root{} + rp := root.GetOrCreateRoutingPolicy() + // POLICY-MATCH-ALL + cfgplugins.AddPrefixSetPolicy(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: matchAllPolicy, StatementNames: []string{"10"}, PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + + cfgplugins.AddPrefixSetPolicyWithMatch(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-PREFIX-SET-A", StatementNames: []string{"10"}, PrefixSetNames: []string{"PREFIX-SET-A"}, PrefixList: policyIPv4Prefixes, PrefixMode: "exact", PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicyWithMatch(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-PREFIX-SET-B", StatementNames: []string{"10"}, PrefixSetNames: []string{"PREFIX-SET-B"}, PrefixList: policyIPv6Prefixes, PrefixMode: "exact", PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicy(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-SUBNET-V4", StatementNames: []string{"10"}, PrefixSetNames: []string{"PREFIX-SET-SUBNET-V4"}, MatchPrefixSet: true, PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicy(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-SUBNET-V6", StatementNames: []string{"10"}, PrefixSetNames: []string{"PREFIX-SET-SUBNET-V6"}, MatchPrefixSet: true, PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicy(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-MULTI-STMT", StatementNames: []string{"10", "20"}, PrefixSetNames: []string{"PREFIX-SET-A", "PREFIX-SET-SUBNET"}, MatchPrefixSet: true, PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicy(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-DENY-PREFIX-SET-A", StatementNames: []string{"10", "20"}, PrefixSetNames: []string{"PREFIX-SET-A", ""}, MatchPrefixSet: true, PrefixDeny: true, PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicy(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-TAG-MATCH", StatementNames: []string{"10"}, MatchPrefixSet: true, SetTag: true, PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicyWithMatch(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-PREFIX-SET-VRF-A", StatementNames: []string{"10"}, PrefixSetNames: []string{"PREFIX-SET-VRF-A"}, PrefixList: []string{vrfV4Pfx}, PrefixMode: "24..32", PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + cfgplugins.AddPrefixSetPolicyWithMatch(t, rp, cfgplugins.PrefixSetPolicyParams{PolicyName: "POLICY-PREFIX-SET-VRF-B", StatementNames: []string{"20"}, PrefixSetNames: []string{"PREFIX-SET-VRF-B"}, PrefixList: []string{vrfV6Pfx}, PrefixMode: "65..128", PolicyResult: oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE}) + + configureGlobalFilterPolicies(t, dut, v4Policy, v6Policy, deviations.DefaultNetworkInstance(dut)) + gnmi.BatchReplace(batch, gnmi.OC().RoutingPolicy().Config(), rp) + batch.Set(t, dut) +} + +// configureStaticRoutes installs a static route into the default NI and non default NI. +func configureStaticRoutes(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, defaultPrefixes, vrfPrefixes []string, nhIP, vrfNhIP string, indx int) { + t.Helper() + for idx, prefix := range defaultPrefixes { + mustConfigureStaticRoute(t, dut, batch, deviations.DefaultNetworkInstance(dut), prefix, fmt.Sprintf("%d", idx+indx), nhIP) + } + // ------------------------------------------------------------ + // VRF-A IPv4 and IPv6 routes + // ------------------------------------------------------------ + for idx, prefix := range vrfPrefixes { + mustConfigureStaticRoute(t, dut, batch, vrfName, prefix, fmt.Sprintf("%d", idx+indx+200), vrfNhIP) + } + batch.Set(t, dut) +} + +// mustConfigureStaticRoute installs a static route into the default NI. +func mustConfigureStaticRoute(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, niName, ipRoutePfx, indx, nxtIP string) { + t.Helper() + staticRoute := &cfgplugins.StaticRouteCfg{ + NetworkInstance: niName, + Prefix: ipRoutePfx, + NextHops: map[string]oc.NetworkInstance_Protocol_Static_NextHop_NextHop_Union{ + indx: oc.UnionString(nxtIP), + }, + } + + if _, err := cfgplugins.NewStaticRouteCfg(batch, staticRoute, dut); err != nil { + t.Fatalf("Failed to configure static route %s: %v", ipRoutePfx, err) + } +} + +// mustAddSingleStaticRoute adds one static route. +func mustAddSingleStaticRoute(t *testing.T, dut *ondatra.DUTDevice, niName, prefix, index, nextHop string) { + t.Helper() + batch := &gnmi.SetBatch{} + staticRoute := &cfgplugins.StaticRouteCfg{ + NetworkInstance: niName, + Prefix: prefix, + NextHops: map[string]oc.NetworkInstance_Protocol_Static_NextHop_NextHop_Union{ + index: oc.UnionString(nextHop), + }, + } + if _, err := cfgplugins.NewStaticRouteCfg(batch, staticRoute, dut); err != nil { + t.Fatalf("Failed creating static route %s: %v", prefix, err) + } + + batch.Set(t, dut) +} + +// verifyPrefixesPresent validates expected prefixes exist. +func verifyPrefixesPresent(t *testing.T, aft *aftcache.AFTData, prefixes []string) { + t.Helper() + + for _, pfx := range prefixes { + if _, ok := aft.Prefixes[pfx]; !ok { + t.Errorf("expected prefix missing: %s", pfx) + } + } +} + +// verifyPrefixesAbsent validates prefixes do not exist. +func verifyPrefixesAbsent(t *testing.T, aft *aftcache.AFTData, prefixes []string) { + t.Helper() + for _, pfx := range prefixes { + if _, ok := aft.Prefixes[pfx]; ok { + t.Errorf("unexpected prefix present: %s", pfx) + } + } +} diff --git a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/metadata.textproto b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/metadata.textproto index 68f88e31a50..b0e81bc5a7c 100644 --- a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/metadata.textproto +++ b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/metadata.textproto @@ -5,3 +5,13 @@ uuid: "82a0e32f-4f79-43dc-a2f3-da48275c539e" plan_id: "AFT-6.4" description: "AFT Prefix Filtering Dynamic Updates" testbed: TESTBED_DUT_ATE_2LINKS +platform_exceptions: { + platform: { + vendor: ARISTA + } + deviations: { + interface_enabled: true + default_network_instance: "default" + afts_global_filter_policy_oc_unsupported: true + } +} diff --git a/internal/cfgplugins/policyforwarding.go b/internal/cfgplugins/policyforwarding.go index 8b3f1f9b4eb..9b8ee4c790b 100644 --- a/internal/cfgplugins/policyforwarding.go +++ b/internal/cfgplugins/policyforwarding.go @@ -1561,3 +1561,81 @@ func ConfigureCLIDecapVRFMode(t *testing.T, dut *ondatra.DUTDevice) { t.Log("Enabling next-hop decapsulation VRF mode") helpers.GnmiCLIConfig(t, dut, cliConfig) } + +// PrefixSetPolicyParams has the parameters to configure prefix set policy. +type PrefixSetPolicyParams struct { + PolicyName string + StatementNames []string + PrefixSetNames []string + MatchPrefixSet bool + SetTag bool + PrefixList []string + PrefixMode string + PrefixDeny bool + PolicyResult oc.E_RoutingPolicy_PolicyResultType +} + +// AddPrefixSetPolicy creates a routing policy statement that matches the specified prefix set and applies the supplied policy result. +func AddPrefixSetPolicy(t *testing.T, rp *oc.RoutingPolicy, cfg PrefixSetPolicyParams) { + t.Helper() + pd := rp.GetOrCreatePolicyDefinition(cfg.PolicyName) + for stIndex, stName := range cfg.StatementNames { + stmt, err := pd.AppendNewStatement(stName) + if err != nil { + t.Fatalf("AppendNewStatement failed: %v", err) + } + if cfg.MatchPrefixSet { + if cfg.SetTag { + stmt.GetOrCreateConditions().GetOrCreateMatchTagSet().TagSet = ygot.String("999") + } else { + if cfg.PrefixSetNames[stIndex] != "" { + match := stmt.GetOrCreateConditions().GetOrCreateMatchPrefixSet() + match.PrefixSet = ygot.String(cfg.PrefixSetNames[stIndex]) + } + } + } + if cfg.PrefixDeny && stIndex == 0 { + stmt.GetOrCreateActions().PolicyResult = oc.RoutingPolicy_PolicyResultType_REJECT_ROUTE + continue + } + stmt.GetOrCreateActions().PolicyResult = cfg.PolicyResult + } +} + +// PrefixParams has prefix parameters to create prefix. +type PrefixParams struct { + PrefixName string + MaskRange string +} + +// createPrefixSetPolicy creates a routing policy that matches a prefix-set and accepts routes matching the configured prefixes. +func AddPrefixSetPolicyWithMatch(t *testing.T, rp *oc.RoutingPolicy, cfg PrefixSetPolicyParams) { + t.Helper() + ps := rp.GetOrCreateDefinedSets().GetOrCreatePrefixSet(cfg.PrefixSetNames[0]) + + for _, prefix := range cfg.PrefixList { + addPrefix(t, ps, prefix, cfg.PrefixMode) + } + + pd := rp.GetOrCreatePolicyDefinition(cfg.PolicyName) + for stIndex, stName := range cfg.StatementNames { + stmt, err := pd.AppendNewStatement(stName) + if err != nil { + t.Fatalf("Failed to append statement: %v", err) + } + match := stmt.GetOrCreateConditions().GetOrCreateMatchPrefixSet() + + match.PrefixSet = ygot.String(cfg.PrefixSetNames[stIndex]) + match.MatchSetOptions = oc.E_RoutingPolicy_MatchSetOptionsRestrictedType(oc.RoutingPolicy_MatchSetOptionsType_ANY) + stmt.GetOrCreateActions().PolicyResult = oc.RoutingPolicy_PolicyResultType_ACCEPT_ROUTE + } +} + +// addPrefix adds prefix-set entry. +func addPrefix(t *testing.T, ps *oc.RoutingPolicy_DefinedSets_PrefixSet, prefix, maskRange string) { + t.Helper() + p := ps.GetOrCreatePrefix(prefix, maskRange) + + p.IpPrefix = ygot.String(prefix) + p.MasklengthRange = ygot.String(maskRange) +} diff --git a/internal/deviations/deviations.go b/internal/deviations/deviations.go index da2467cbae1..e38c28c26d1 100644 --- a/internal/deviations/deviations.go +++ b/internal/deviations/deviations.go @@ -2245,3 +2245,8 @@ func UseInterfaceNameForIBGPNeighborTransportIpv4LocalAddress(dut *ondatra.DUTDe func InterfaceIDFormatRequiredForPolicyForwarding(dut *ondatra.DUTDevice) bool { return lookupDUTDeviations(dut).GetInterfaceIdFormatRequiredForPolicyForwarding() } + +// AftsGlobalFilterPolicyOCUnsupported returns true if the device does not support Afts Global Filter paths /network-instances/network-instance/afts/global-filter/config/. +func AftsGlobalFilterPolicyOCUnsupported(dut *ondatra.DUTDevice) bool { + return lookupDUTDeviations(dut).GetAftsGlobalFilterPolicyOcUnsupported() +} diff --git a/internal/telemetry/aftcache/aft_cache.go b/internal/telemetry/aftcache/aft_cache.go index 3939fd3d039..c90c82754e0 100644 --- a/internal/telemetry/aftcache/aft_cache.go +++ b/internal/telemetry/aftcache/aft_cache.go @@ -744,7 +744,13 @@ func InitialSyncStoppingCondition(t *testing.T, dut *ondatra.DUTDevice, wantPref return false, nil } ss.missingPrefixes = make(map[string]bool) // All prefixes are present, so clear the list. - + noIPv4NHValidation := len(wantIPV4NHs) == 0 + noIPv6NHValidation := len(wantIPV6NHs) == 0 + if noIPv4NHValidation && noIPv6NHValidation { + t.Logf("%s Prefix validation completed successfully. "+"Skipping NH validation because no NH expectations were provided.", prefix) + t.Logf("%s Initial sync stopping condition took %.2f sec", prefix, time.Since(start).Seconds()) + return true, nil + } // Check next hops. checkNHStart := time.Now() nCorrect := 0 @@ -1131,3 +1137,125 @@ func writeNotifications(t *testing.T, notifications []*gnmipb.SubscribeResponse, } return path, nil } + +// Notifications return the AFT stream notifications. +func (ss *AFTStreamSession) Notifications() []*gnmipb.SubscribeResponse { + return ss.notifications +} + +// notificationMatch tracks whether the expected update/delete notifications have been observed in the gNMI stream. +type notificationMatch struct { + updateFound bool + deleteFound bool +} + +// NotificationExpectation defines prefixes that should appear as UPDATE and/or DELETE notifications in the gNMI stream. +type NotificationExpectation struct { + AddPrefix string + DeletePrefix string +} + +// WaitForUpdateDeleteNotification returns a PeriodicHook that waits until: +// +// - an UPDATE notification is received for updatePrefix +// - a DELETE notification is received for deletePrefix +// +// The hook returns true only after both notifications have been observed. +func WaitForUpdateDeleteNotification(t *testing.T, cfg NotificationExpectation) PeriodicHook { + t.Helper() + + return PeriodicHook{ + Description: fmt.Sprintf("Wait for UPDATE(%s) and DELETE(%s) notifications", cfg.AddPrefix, cfg.DeletePrefix), + PeriodicFunc: func(ss *AFTStreamSession) (bool, error) { + match := notificationMatch{} + for _, resp := range ss.Notifications() { + update := resp.GetUpdate() + if update == nil { + continue + } + + if hasUpdateNotification(update, cfg.AddPrefix) { + match.updateFound = true + } + + if hasDeleteNotification(update, cfg.DeletePrefix) { + match.deleteFound = true + } + + if match.updateFound && match.deleteFound { + t.Logf("Update and Delete notifications received for %s, %s", cfg.AddPrefix, cfg.DeletePrefix) + return true, nil + } + } + return false, fmt.Errorf("update and delete notifications are not received for %s, %s", cfg.AddPrefix, cfg.DeletePrefix) + }, + } +} + +// WaitForDeleteNotification returns a PeriodicHook that waits until a DELETE notification is received for the specified prefix. +func WaitForDeleteNotification(t *testing.T, cfg NotificationExpectation) PeriodicHook { + return PeriodicHook{ + Description: fmt.Sprintf("Wait for delete notification: %s", cfg.DeletePrefix), + PeriodicFunc: func(ss *AFTStreamSession) (bool, error) { + for _, resp := range ss.Notifications() { + update := resp.GetUpdate() + if update == nil { + continue + } + if hasDeleteNotification(update, cfg.DeletePrefix) { + t.Logf("Delete notification received for %s", cfg.DeletePrefix) + return true, nil + } + } + return false, fmt.Errorf("delete notification not received for prefix %s", cfg.DeletePrefix) + }, + } +} + +// WaitForUpdateNotification returns a PeriodicHook that waits until an UPDATE notification is received for the specified prefix. +func WaitForUpdateNotification(t *testing.T, cfg NotificationExpectation) PeriodicHook { + return PeriodicHook{ + Description: fmt.Sprintf("Wait for update notification for %s", cfg.AddPrefix), + PeriodicFunc: func(ss *AFTStreamSession) (bool, error) { + for _, resp := range ss.Notifications() { + update := resp.GetUpdate() + if update == nil { + continue + } + if hasUpdateNotification(update, cfg.AddPrefix) { + t.Logf("Update notification received for %s", cfg.AddPrefix) + return true, nil + } + } + return false, fmt.Errorf("update notification not received for prefix %s", cfg.AddPrefix) + }, + } +} + +// hasUpdateNotification returns true if the specified prefix appears in any UPDATE path within the notification. +func hasUpdateNotification(update *gnmipb.Notification, prefix string) bool { + for _, upd := range update.GetUpdate() { + for _, elem := range upd.GetPath().GetElem() { + for _, keyVal := range elem.GetKey() { + if keyVal == prefix { + return true + } + } + } + } + return false +} + +// hasDeleteNotification returns true if the specified prefix appears in any DELETE path within the notification. +func hasDeleteNotification(update *gnmipb.Notification, prefix string) bool { + for _, del := range update.GetDelete() { + for _, elem := range del.GetElem() { + for _, keyVal := range elem.GetKey() { + if keyVal == prefix { + return true + } + } + } + } + return false +} diff --git a/proto/metadata.proto b/proto/metadata.proto index cb687cfa6ac..e8521d214e4 100644 --- a/proto/metadata.proto +++ b/proto/metadata.proto @@ -1374,6 +1374,10 @@ message Metadata { // Cisco: https://partnerissuetracker.corp.google.com/u/0/issues/523054650 bool interface_id_format_required_for_policy_forwarding = 434; + // Arista: b/514565554 + // Devices that do not support afts global filter policy OC + bool afts_global_filter_policy_oc_unsupported = 435; + // Reserved field numbers and identifiers. reserved 84, 9, 28, 20, 38, 43, 90, 97, 55, 89, 19, 36, 35, 40, 113, 131, 141, 173, 234, 254, 231, 300, 241, 49; } diff --git a/proto/metadata_go_proto/metadata.pb.go b/proto/metadata_go_proto/metadata.pb.go index b5406f7963b..3ba57a299bf 100644 --- a/proto/metadata_go_proto/metadata.pb.go +++ b/proto/metadata_go_proto/metadata.pb.go @@ -14,20 +14,19 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 -// protoc v7.35.0 +// protoc-gen-go v1.36.8 +// protoc v3.21.12 // source: metadata.proto package metadata_go_proto import ( - reflect "reflect" - sync "sync" - unsafe "unsafe" - proto "github.com/openconfig/ondatra/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" ) const ( @@ -1483,8 +1482,11 @@ type Metadata_Deviations struct { // .subinterface index with Interface-ref container specifically for policy forwarding usecase // Cisco: https://partnerissuetracker.corp.google.com/u/0/issues/523054650 InterfaceIdFormatRequiredForPolicyForwarding bool `protobuf:"varint,434,opt,name=interface_id_format_required_for_policy_forwarding,json=interfaceIdFormatRequiredForPolicyForwarding,proto3" json:"interface_id_format_required_for_policy_forwarding,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Arista: b/514565554 + // Devices that do not support afts global filter policy OC + AftsGlobalFilterPolicyOcUnsupported bool `protobuf:"varint,435,opt,name=afts_global_filter_policy_oc_unsupported,json=aftsGlobalFilterPolicyOcUnsupported,proto3" json:"afts_global_filter_policy_oc_unsupported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Metadata_Deviations) Reset() { @@ -4310,6 +4312,13 @@ func (x *Metadata_Deviations) GetInterfaceIdFormatRequiredForPolicyForwarding() return false } +func (x *Metadata_Deviations) GetAftsGlobalFilterPolicyOcUnsupported() bool { + if x != nil { + return x.AftsGlobalFilterPolicyOcUnsupported + } + return false +} + type Metadata_PlatformExceptions struct { state protoimpl.MessageState `protogen:"open.v1"` Platform *Metadata_Platform `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"` @@ -4366,7 +4375,7 @@ var File_metadata_proto protoreflect.FileDescriptor const file_metadata_proto_rawDesc = "" + "\n" + - "\x0emetadata.proto\x12\x12openconfig.testing\x1a1github.com/openconfig/ondatra/proto/testbed.proto\"\x87\xf4\x01\n" + + "\x0emetadata.proto\x12\x12openconfig.testing\x1a1github.com/openconfig/ondatra/proto/testbed.proto\"\xdf\xf4\x01\n" + "\bMetadata\x12\x12\n" + "\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x17\n" + "\aplan_id\x18\x02 \x01(\tR\x06planId\x12 \n" + @@ -4378,7 +4387,7 @@ const file_metadata_proto_rawDesc = "" + "\bPlatform\x12.\n" + "\x06vendor\x18\x01 \x01(\x0e2\x16.ondatra.Device.VendorR\x06vendor\x120\n" + "\x14hardware_model_regex\x18\x03 \x01(\tR\x12hardwareModelRegex\x124\n" + - "\x16software_version_regex\x18\x04 \x01(\tR\x14softwareVersionRegexJ\x04\b\x02\x10\x03R\x0ehardware_model\x1a\xd3\xe9\x01\n" + + "\x16software_version_regex\x18\x04 \x01(\tR\x14softwareVersionRegexJ\x04\b\x02\x10\x03R\x0ehardware_model\x1a\xab\xea\x01\n" + "\n" + "Deviations\x120\n" + "\x14ipv4_missing_enabled\x18\x01 \x01(\bR\x12ipv4MissingEnabled\x129\n" + @@ -4783,7 +4792,8 @@ const file_metadata_proto_rawDesc = "" + "\x19dhcp_relay_oc_unsupported\x18\xaf\x03 \x01(\bR\x16dhcpRelayOcUnsupported\x12V\n" + "(p4rt_explicit_table_entry_per_controller\x18\xb0\x03 \x01(\bR#p4rtExplicitTableEntryPerController\x12\x84\x01\n" + "Ause_interface_name_for_ibgp_neighbor_transport_ipv4_local_address\x18\xb1\x03 \x01(\bR8useInterfaceNameForIbgpNeighborTransportIpv4LocalAddress\x12i\n" + - "2interface_id_format_required_for_policy_forwarding\x18\xb2\x03 \x01(\bR,interfaceIdFormatRequiredForPolicyForwardingJ\x04\bT\x10UJ\x04\b\t\x10\n" + + "2interface_id_format_required_for_policy_forwarding\x18\xb2\x03 \x01(\bR,interfaceIdFormatRequiredForPolicyForwarding\x12V\n" + + "(afts_global_filter_policy_oc_unsupported\x18\xb3\x03 \x01(\bR#aftsGlobalFilterPolicyOcUnsupportedJ\x04\bT\x10UJ\x04\b\t\x10\n" + "J\x04\b\x1c\x10\x1dJ\x04\b\x14\x10\x15J\x04\b&\x10'J\x04\b+\x10,J\x04\bZ\x10[J\x04\ba\x10bJ\x04\b7\x108J\x04\bY\x10ZJ\x04\b\x13\x10\x14J\x04\b$\x10%J\x04\b#\x10$J\x04\b(\x10)J\x04\bq\x10rJ\x06\b\x83\x01\x10\x84\x01J\x06\b\x8d\x01\x10\x8e\x01J\x06\b\xad\x01\x10\xae\x01J\x06\b\xea\x01\x10\xeb\x01J\x06\b\xfe\x01\x10\xff\x01J\x06\b\xe7\x01\x10\xe8\x01J\x06\b\xac\x02\x10\xad\x02J\x06\b\xf1\x01\x10\xf2\x01J\x04\b1\x102\x1a\xa0\x01\n" + "\x12PlatformExceptions\x12A\n" + "\bplatform\x18\x01 \x01(\v2%.openconfig.testing.Metadata.PlatformR\bplatform\x12G\n" + From cd359e1310f7e7799f88a8d729eb371829a68846 Mon Sep 17 00:00:00 2001 From: moharedd Date: Sat, 13 Jun 2026 04:22:21 +0000 Subject: [PATCH 2/3] Fixed Gemini review comments --- .../afts_prefix_filtering_dynamic_test.go | 43 ++++++++++--------- internal/deviations/deviations.go | 1 + internal/telemetry/aftcache/aft_cache.go | 26 ++++++++--- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go index 38e29b3de50..06ba3f1684f 100644 --- a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go +++ b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go @@ -32,22 +32,23 @@ import ( ) const ( - vrfName = "VRF-A" - v4PfxSet = "PREFIX-SET-A" - v6PfxSet = "PREFIX-SET-B" - v4Policy = "POLICY-PREFIX-SET-A" - v6Policy = "POLICY-PREFIX-SET-B" - matchAllPolicy = "POLICY-MATCH-ALL" - subscriptionWait = 3 * time.Minute - prefixAft1V4 = "198.51.100.0/24" - prefixAft2V4 = "203.0.113.0/28" - prefixAft3V4 = "192.0.2.0/24" - prefixAft1V6 = "2001:db8:2::/64" - prefixAft2V6 = "2001:db8:2::1/128" - prefixAft3V6 = "2001:db8:2::2/128" - vrfV4Pfx = "100.64.1.0/24" - vrfV6Pfx = "2001:db8:3::/64" - maskRange = "exact" + vrfName = "VRF-A" + v4PfxSet = "PREFIX-SET-A" + v6PfxSet = "PREFIX-SET-B" + v4Policy = "POLICY-PREFIX-SET-A" + v6Policy = "POLICY-PREFIX-SET-B" + matchAllPolicy = "POLICY-MATCH-ALL" + subscriptionWait = 3 * time.Minute + prefixAft1V4 = "198.51.100.0/24" + prefixAft2V4 = "203.0.113.0/28" + prefixAft3V4 = "192.0.2.0/24" + prefixAft1V6 = "2001:db8:2::/64" + prefixAft2V6 = "2001:db8:2::1/128" + prefixAft3V6 = "2001:db8:2::2/128" + vrfV4Pfx = "100.64.1.0/24" + vrfV6Pfx = "2001:db8:3::/64" + maskRange = "exact" + notificationWaitTime = 30 * time.Second ) var ( @@ -308,7 +309,8 @@ func validateIPv6DynamicUpdates(t *testing.T, dut *ondatra.DUTDevice) { // validateDynamicUpdates validates dynamic AFT prefix filtering behavior for both IPv4 and IPv6 route policies. func validateDynamicUpdates(t *testing.T, dut *ondatra.DUTDevice, pArgs dynamicUpdateTestParams) { t.Helper() - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() configureGlobalFilterPolicies(t, dut, pArgs.policyName, "", deviations.DefaultNetworkInstance(dut)) wantPrefixes := map[string]bool{ @@ -352,7 +354,7 @@ func validateDynamicUpdates(t *testing.T, dut *ondatra.DUTDevice, pArgs dynamicU } verifyPrefixesPresent(t, addAFT, []string{pArgs.prefix3}) // Wait until notification received or timeout - runCollector(ctx, t, addCollector, aftcache.WaitForUpdateNotification(t, aftcache.NotificationExpectation{AddPrefix: pArgs.prefix3})) + runCollector(ctx, t, addCollector, aftcache.WaitForUpdateNotification(t, aftcache.NotificationExpectation{AddPrefix: pArgs.prefix3, NotificationWait: notificationWaitTime})) // ------------------------------------------------------------ // AFT-6.4.X.2 Delete Prefix @@ -370,7 +372,7 @@ func validateDynamicUpdates(t *testing.T, dut *ondatra.DUTDevice, pArgs dynamicU verifyPrefixRemovedFromPrefixSet(t, dut, pArgs.prefixSet, pArgs.prefix1, pArgs.maskRange) verifyPrefixesAbsent(t, delaft, []string{pArgs.prefix1}) // Wait until notification received or timeout - runCollector(ctx, t, deleteCollector, aftcache.WaitForDeleteNotification(t, aftcache.NotificationExpectation{DeletePrefix: pArgs.prefix1})) + runCollector(ctx, t, deleteCollector, aftcache.WaitForDeleteNotification(t, aftcache.NotificationExpectation{DeletePrefix: pArgs.prefix1, NotificationWait: notificationWaitTime})) // ------------------------------------------------------------ // AFT-6.4.X.3 Atomic Add/Delete @@ -389,7 +391,7 @@ func validateDynamicUpdates(t *testing.T, dut *ondatra.DUTDevice, pArgs dynamicU verifyPrefixesPresent(t, swapaft, []string{pArgs.prefix1}) verifyPrefixesAbsent(t, swapaft, []string{pArgs.prefix2}) // Wait until notification received or timeout - runCollector(ctx, t, swapCollector, aftcache.WaitForUpdateDeleteNotification(t, aftcache.NotificationExpectation{AddPrefix: pArgs.prefix1, DeletePrefix: pArgs.prefix2})) + runCollector(ctx, t, swapCollector, aftcache.WaitForUpdateDeleteNotification(t, aftcache.NotificationExpectation{AddPrefix: pArgs.prefix1, DeletePrefix: pArgs.prefix2, NotificationWait: notificationWaitTime})) } // newCollector creates and returns a new AFT stream session. If debug_notifications is enabled, all received gNMI notifications are recorded in memory for later inspection and troubleshooting. @@ -457,6 +459,7 @@ func configureGlobalFilterPolicies(t *testing.T, dut *ondatra.DUTDevice, ipv4Pol switch dut.Vendor() { case ondatra.ARISTA: t.Log("Skipping AFT global-filter attachment: unsupported on EOS") + return } } else { // TODO: Enable the following code once OC supports AFTs global filter configuration. diff --git a/internal/deviations/deviations.go b/internal/deviations/deviations.go index e38c28c26d1..abd59182c8c 100644 --- a/internal/deviations/deviations.go +++ b/internal/deviations/deviations.go @@ -2247,6 +2247,7 @@ func InterfaceIDFormatRequiredForPolicyForwarding(dut *ondatra.DUTDevice) bool { } // AftsGlobalFilterPolicyOCUnsupported returns true if the device does not support Afts Global Filter paths /network-instances/network-instance/afts/global-filter/config/. +// Arista: https://partnerissuetracker.corp.google.com/issues/514565554 func AftsGlobalFilterPolicyOCUnsupported(dut *ondatra.DUTDevice) bool { return lookupDUTDeviations(dut).GetAftsGlobalFilterPolicyOcUnsupported() } diff --git a/internal/telemetry/aftcache/aft_cache.go b/internal/telemetry/aftcache/aft_cache.go index c90c82754e0..7bd2104a844 100644 --- a/internal/telemetry/aftcache/aft_cache.go +++ b/internal/telemetry/aftcache/aft_cache.go @@ -1151,8 +1151,9 @@ type notificationMatch struct { // NotificationExpectation defines prefixes that should appear as UPDATE and/or DELETE notifications in the gNMI stream. type NotificationExpectation struct { - AddPrefix string - DeletePrefix string + AddPrefix string + DeletePrefix string + NotificationWait time.Duration } // WaitForUpdateDeleteNotification returns a PeriodicHook that waits until: @@ -1163,7 +1164,7 @@ type NotificationExpectation struct { // The hook returns true only after both notifications have been observed. func WaitForUpdateDeleteNotification(t *testing.T, cfg NotificationExpectation) PeriodicHook { t.Helper() - + start := time.Now() return PeriodicHook{ Description: fmt.Sprintf("Wait for UPDATE(%s) and DELETE(%s) notifications", cfg.AddPrefix, cfg.DeletePrefix), PeriodicFunc: func(ss *AFTStreamSession) (bool, error) { @@ -1187,13 +1188,18 @@ func WaitForUpdateDeleteNotification(t *testing.T, cfg NotificationExpectation) return true, nil } } - return false, fmt.Errorf("update and delete notifications are not received for %s, %s", cfg.AddPrefix, cfg.DeletePrefix) + if time.Since(start) > cfg.NotificationWait { + return false, fmt.Errorf("update and delete notifications are not received for %s, %s", cfg.AddPrefix, cfg.DeletePrefix) + } + return false, nil }, } } // WaitForDeleteNotification returns a PeriodicHook that waits until a DELETE notification is received for the specified prefix. func WaitForDeleteNotification(t *testing.T, cfg NotificationExpectation) PeriodicHook { + t.Helper() + start := time.Now() return PeriodicHook{ Description: fmt.Sprintf("Wait for delete notification: %s", cfg.DeletePrefix), PeriodicFunc: func(ss *AFTStreamSession) (bool, error) { @@ -1207,13 +1213,18 @@ func WaitForDeleteNotification(t *testing.T, cfg NotificationExpectation) Period return true, nil } } - return false, fmt.Errorf("delete notification not received for prefix %s", cfg.DeletePrefix) + if time.Since(start) > cfg.NotificationWait { + return false, fmt.Errorf("delete notification not received for prefix %s", cfg.DeletePrefix) + } + return false, nil }, } } // WaitForUpdateNotification returns a PeriodicHook that waits until an UPDATE notification is received for the specified prefix. func WaitForUpdateNotification(t *testing.T, cfg NotificationExpectation) PeriodicHook { + t.Helper() + start := time.Now() return PeriodicHook{ Description: fmt.Sprintf("Wait for update notification for %s", cfg.AddPrefix), PeriodicFunc: func(ss *AFTStreamSession) (bool, error) { @@ -1227,7 +1238,10 @@ func WaitForUpdateNotification(t *testing.T, cfg NotificationExpectation) Period return true, nil } } - return false, fmt.Errorf("update notification not received for prefix %s", cfg.AddPrefix) + if time.Since(start) > cfg.NotificationWait { + return false, fmt.Errorf("update notification not received for prefix %s after %v", cfg.AddPrefix, cfg.NotificationWait) + } + return false, nil }, } } From 6f3c4299ea50ef1d871129054b088325a38d68d5 Mon Sep 17 00:00:00 2001 From: moharedd Date: Tue, 16 Jun 2026 16:17:10 +0000 Subject: [PATCH 3/3] Fixed review comment --- .../afts_prefix_filtering_dynamic_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go index 06ba3f1684f..fc8bb353593 100644 --- a/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go +++ b/feature/afts/filtered_streaming/otg_tests/afts_prefix_filtering_dynamic/afts_prefix_filtering_dynamic_test.go @@ -195,7 +195,7 @@ func configureDUT(t *testing.T, dut *ondatra.DUTDevice) *gnmi.SetBatch { // configureDUTInterface configure interfaces on DUT. func configureDUTInterface(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, attrs *attrs.Attributes, p *ondatra.Port) { t.Helper() - d := gnmi.OC() + ocPath := gnmi.OC() i := attrs.NewOCInterface(p.Name(), dut) i.Description = ygot.String(attrs.Desc) i.Type = oc.IETFInterfaces_InterfaceType_ethernetCsmacd @@ -214,16 +214,16 @@ func configureDUTInterface(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.Set av6 := i6.GetOrCreateAddress(attrs.IPv6) av6.PrefixLength = ygot.Uint8(attrs.IPv6Len) - gnmi.BatchUpdate(batch, d.Interface(p.Name()).Config(), i) + gnmi.BatchUpdate(batch, ocPath.Interface(p.Name()).Config(), i) } // configureDUTPort configure DUT ports. func configureDUTPort(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, attrs *attrs.Attributes, p *ondatra.Port, niName string) { t.Helper() - d := gnmi.OC() + ocPath := gnmi.OC() cfgplugins.AssignToNetworkInstance(t, dut, p.Name(), niName, 0) i := attrs.NewOCInterface(p.Name(), dut) - gnmi.BatchUpdate(batch, d.Interface(p.Name()).Config(), i) + gnmi.BatchUpdate(batch, ocPath.Interface(p.Name()).Config(), i) } // configureHardwareInit sets up the initial hardware configuration on the DUT. It pushes hardware initialization configs for VRF Selection Extended feature and Policy Forwarding feature.