Add truncate instruction and support it with PI framework

To support truncate by P4Runtime clone/mirror session, we need to pass the
truncate size/length from ONOS northbound to the southbound.
As discussed in the SDFabric syncup, we decide to pass this information via
the instruction in group bucket so applications or pipeliners can simply
reuse current APIs.

Change-Id: I15cc822b7c8008b6b9f8b02f3f399769ae396ef0
(cherry picked from commit 9f94a13bf5695996708eedc17166b5b09308147f)
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImpl.java
index 53fb9d2..3407cf2 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImpl.java
@@ -16,13 +16,16 @@
 
 package org.onosproject.net.pi.impl;
 
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import org.onosproject.net.Device;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.flow.instructions.Instruction;
 import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.flow.instructions.Instructions.TruncateInstruction;
 import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
 import org.onosproject.net.group.GroupDescription;
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
@@ -69,23 +72,42 @@
 
         checkNotNull(group);
 
-        final List<Instruction> instructions = group.buckets().buckets().stream()
-                .flatMap(b -> b.treatment().allInstructions().stream())
-                .collect(Collectors.toList());
-
-        final boolean hasNonOutputInstr = instructions.stream()
-                .anyMatch(i -> !i.type().equals(Instruction.Type.OUTPUT));
-
-        if (instructions.size() != group.buckets().buckets().size()
-                || hasNonOutputInstr) {
-            throw new PiTranslationException(
-                    "support only groups with just one OUTPUT instruction per bucket");
+        final List<OutputInstruction> outInstructions = Lists.newArrayList();
+        int truncateMaxLen = PiCloneSessionEntry.DO_NOT_TRUNCATE;
+        for (GroupBucket bucket : group.buckets().buckets()) {
+            int numInstructionsInBucket = bucket.treatment().allInstructions().size();
+            List<OutputInstruction> outputs =
+                    getInstructions(bucket, Instruction.Type.OUTPUT, OutputInstruction.class);
+            List<TruncateInstruction> truncates =
+                    getInstructions(bucket, Instruction.Type.TRUNCATE, TruncateInstruction.class);
+            if (outputs.size() != 1) {
+                throw new PiTranslationException(
+                        "support only groups with just one OUTPUT instruction per bucket");
+            }
+            outInstructions.add(outputs.get(0));
+            if (truncates.size() != 0) {
+                if (group.type() != GroupDescription.Type.CLONE) {
+                    throw new PiTranslationException("only CLONE group support truncate instruction");
+                }
+                if (truncates.size() != 1) {
+                    throw new PiTranslationException(
+                            "support only groups with just one TRUNCATE instruction per bucket");
+                }
+                int truncateInstMaxLen = truncates.get(0).maxLen();
+                if (truncateMaxLen != PiCloneSessionEntry.DO_NOT_TRUNCATE &&
+                        truncateMaxLen != truncateInstMaxLen) {
+                    throw new PiTranslationException("all TRUNCATE instruction must be the same in a CLONE group");
+                }
+                truncateMaxLen = truncateInstMaxLen;
+            } else if (truncateMaxLen != PiCloneSessionEntry.DO_NOT_TRUNCATE) {
+                // No truncate instruction found in this bucket, but previous bucket contains one.
+                throw new PiTranslationException("all TRUNCATE instruction must be the same in a CLONE group");
+            }
+            if (numInstructionsInBucket != outputs.size() + truncates.size()) {
+                throw new PiTranslationException("bucket contains unsupported instruction(s)");
+            }
         }
 
-        final List<OutputInstruction> outInstructions = instructions.stream()
-                .map(i -> (OutputInstruction) i)
-                .collect(Collectors.toList());
-
         switch (group.type()) {
             case ALL:
                 return PiMulticastGroupEntry.builder()
@@ -96,6 +118,7 @@
                 return PiCloneSessionEntry.builder()
                         .withSessionId(group.id().id())
                         .addReplicas(getReplicas(outInstructions, device))
+                        .withMaxPacketLengthBytes(truncateMaxLen)
                         .build();
             default:
                 throw new PiTranslationException(format(
@@ -140,4 +163,13 @@
         }
         return PortNumber.portNumber(mappedPort.get());
     }
+
+    private static <I extends Instruction> List<I> getInstructions(GroupBucket bucket,
+                                                                   Instruction.Type type,
+                                                                   Class<I> instructionClass) {
+        return bucket.treatment().allInstructions().stream()
+                .filter(i -> i.type() == type)
+                .map(instructionClass::cast)
+                .collect(Collectors.toList());
+    }
 }
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImplTest.java
index 8ca78cf..00ad506d 100644
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImplTest.java
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiReplicationGroupTranslatorImplTest.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.testing.EqualsTester;
+import org.junit.Assert;
 import org.junit.Test;
 import org.onosproject.TestApplicationId;
 import org.onosproject.core.ApplicationId;
@@ -40,6 +41,7 @@
 import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
 import org.onosproject.net.pi.runtime.PiPreEntry;
 import org.onosproject.net.pi.runtime.PiPreReplica;
+import org.onosproject.net.pi.service.PiTranslationException;
 
 import java.util.List;
 import java.util.Set;
@@ -57,21 +59,75 @@
     private static final GroupId GROUP_ID = GroupId.valueOf(ENTRY_ID);
     private static final GroupKey GROUP_KEY = new DefaultGroupKey(
             String.valueOf(GROUP_ID.id()).getBytes());
+    private static final int TRUNCATE_MAC_LEN = 1400;
 
     private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(
             allOutputBucket(1),
             allOutputBucket(2),
             allOutputBucket(3));
-    private static final List<GroupBucket> CLONE_BUCKET_LIST = ImmutableList.of(
-            cloneOutputBucket(1),
-            cloneOutputBucket(2),
-            cloneOutputBucket(3));
     private static final List<GroupBucket> BUCKET_LIST_2 = ImmutableList.of(
             allOutputBucket(1),
             allOutputBucket(2),
             allOutputBucket(2),
             allOutputBucket(3),
             allOutputBucket(3));
+    private static final List<GroupBucket> CLONE_BUCKET_LIST = ImmutableList.of(
+            cloneOutputBucket(1),
+            cloneOutputBucket(2),
+            cloneOutputBucket(3));
+    private static final List<GroupBucket> CLONE_BUCKET_LIST_2 = ImmutableList.of(
+            cloneOutputBucket(1, TRUNCATE_MAC_LEN),
+            cloneOutputBucket(2, TRUNCATE_MAC_LEN),
+            cloneOutputBucket(3, TRUNCATE_MAC_LEN));
+    private static final List<GroupBucket> INVALID_BUCKET_LIST = ImmutableList.of(
+            DefaultGroupBucket.createAllGroupBucket(
+                    DefaultTrafficTreatment.emptyTreatment()
+            )
+    );
+    // This is invalid since we can only use truncate instruction in a clone group.
+    private static final List<GroupBucket> INVALID_BUCKET_LIST_2 = ImmutableList.of(
+        DefaultGroupBucket.createAllGroupBucket(
+                DefaultTrafficTreatment.builder()
+                        .setOutput(PortNumber.portNumber(1))
+                        .truncate(TRUNCATE_MAC_LEN)
+                        .build()
+        )
+    );
+    // Unsupported instruction.
+    private static final List<GroupBucket> INVALID_BUCKET_LIST_3 = ImmutableList.of(
+            DefaultGroupBucket.createAllGroupBucket(
+                    DefaultTrafficTreatment.builder()
+                            .setOutput(PortNumber.portNumber(1))
+                            .popVlan()
+                            .build()
+            )
+    );
+    private static final List<GroupBucket> INVALID_CLONE_BUCKET_LIST = ImmutableList.of(
+            DefaultGroupBucket.createCloneGroupBucket(
+                    DefaultTrafficTreatment.emptyTreatment()
+            )
+    );
+    // This is invalid since all truncate instruction must be the same.
+    private static final List<GroupBucket> INVALID_CLONE_BUCKET_LIST_2 = ImmutableList.of(
+            cloneOutputBucket(1, TRUNCATE_MAC_LEN),
+            cloneOutputBucket(2, TRUNCATE_MAC_LEN + 1)
+    );
+    // This is invalid since only one truncate instruction is allowed per bucket.
+    private static final List<GroupBucket> INVALID_CLONE_BUCKET_LIST_3 = ImmutableList.of(
+            DefaultGroupBucket.createCloneGroupBucket(
+                    DefaultTrafficTreatment.builder()
+                            .setOutput(PortNumber.portNumber(1))
+                            .truncate(TRUNCATE_MAC_LEN)
+                            .truncate(TRUNCATE_MAC_LEN)
+                            .build()
+            )
+    );
+    // Inconsistent truncate instructions.
+    private static final List<GroupBucket> INVALID_CLONE_BUCKET_LIST_4 = ImmutableList.of(
+            cloneOutputBucket(1, TRUNCATE_MAC_LEN),
+            cloneOutputBucket(2, TRUNCATE_MAC_LEN),
+            cloneOutputBucket(3)
+    );
 
     private static final Set<PiPreReplica> REPLICAS = ImmutableSet.of(
             new PiPreReplica(PortNumber.portNumber(1), 0),
@@ -89,32 +145,34 @@
                     .withGroupId(GROUP_ID.id())
                     .addReplicas(REPLICAS)
                     .build();
-    private static final PiCloneSessionEntry PI_CLONE_SESSION_ENTRY =
-            PiCloneSessionEntry.builder()
-                    .withSessionId(ENTRY_ID)
-                    .addReplicas(REPLICAS)
-                    .build();
     private static final PiMulticastGroupEntry PI_MULTICAST_GROUP_2 =
             PiMulticastGroupEntry.builder()
                     .withGroupId(GROUP_ID.id())
                     .addReplicas(REPLICAS_2)
                     .build();
+    private static final PiCloneSessionEntry PI_CLONE_SESSION_ENTRY =
+            PiCloneSessionEntry.builder()
+                    .withSessionId(ENTRY_ID)
+                    .addReplicas(REPLICAS)
+                    .build();
+    private static final PiCloneSessionEntry PI_CLONE_SESSION_ENTRY_2 =
+            PiCloneSessionEntry.builder()
+                    .withSessionId(ENTRY_ID)
+                    .addReplicas(REPLICAS)
+                    .withMaxPacketLengthBytes(TRUNCATE_MAC_LEN)
+                    .build();
 
-    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
-    private static final GroupBuckets CLONE_BUCKETS = new GroupBuckets(CLONE_BUCKET_LIST);
-    private static final GroupBuckets BUCKETS_2 = new GroupBuckets(BUCKET_LIST_2);
-
-    private static final GroupDescription ALL_GROUP_DESC = new DefaultGroupDescription(
-            DEVICE_ID, ALL, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
-    private static final Group ALL_GROUP = new DefaultGroup(GROUP_ID, ALL_GROUP_DESC);
-
-    private static final GroupDescription CLONE_GROUP_DESC = new DefaultGroupDescription(
-            DEVICE_ID, CLONE, CLONE_BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
-    private static final Group CLONE_GROUP = new DefaultGroup(GROUP_ID, CLONE_GROUP_DESC);
-
-    private static final GroupDescription ALL_GROUP_DESC_2 = new DefaultGroupDescription(
-            DEVICE_ID, ALL, BUCKETS_2, GROUP_KEY, GROUP_ID.id(), APP_ID);
-    private static final Group ALL_GROUP_2 = new DefaultGroup(GROUP_ID, ALL_GROUP_DESC_2);
+    private static final Group ALL_GROUP = createGroup(BUCKET_LIST, ALL);
+    private static final Group ALL_GROUP_2 = createGroup(BUCKET_LIST_2, ALL);
+    private static final Group CLONE_GROUP = createGroup(CLONE_BUCKET_LIST, CLONE);
+    private static final Group CLONE_GROUP_2 = createGroup(CLONE_BUCKET_LIST_2, CLONE);
+    private static final Group INVALID_ALL_GROUP = createGroup(INVALID_BUCKET_LIST, ALL);
+    private static final Group INVALID_ALL_GROUP_2 = createGroup(INVALID_BUCKET_LIST_2, ALL);
+    private static final Group INVALID_ALL_GROUP_3 = createGroup(INVALID_BUCKET_LIST_3, ALL);
+    private static final Group INVALID_CLONE_GROUP = createGroup(INVALID_CLONE_BUCKET_LIST, CLONE);
+    private static final Group INVALID_CLONE_GROUP_2 = createGroup(INVALID_CLONE_BUCKET_LIST_2, CLONE);
+    private static final Group INVALID_CLONE_GROUP_3 = createGroup(INVALID_CLONE_BUCKET_LIST_3, CLONE);
+    private static final Group INVALID_CLONE_GROUP_4 = createGroup(INVALID_CLONE_BUCKET_LIST_4, CLONE);
 
 
     private static GroupBucket allOutputBucket(int portNum) {
@@ -131,20 +189,96 @@
         return DefaultGroupBucket.createCloneGroupBucket(treatment);
     }
 
+    private static GroupBucket cloneOutputBucket(int portNum, int truncateMaxLen) {
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setOutput(PortNumber.portNumber(portNum))
+                .truncate(truncateMaxLen)
+                .build();
+        return DefaultGroupBucket.createCloneGroupBucket(treatment);
+    }
+
+    private static Group createGroup(List<GroupBucket> bucketList, GroupDescription.Type type) {
+        final GroupBuckets buckets = new GroupBuckets(bucketList);
+        final GroupDescription groupDesc = new DefaultGroupDescription(
+                DEVICE_ID, type, buckets, GROUP_KEY, GROUP_ID.id(), APP_ID);
+        return new DefaultGroup(GROUP_ID, groupDesc);
+    }
+
     @Test
     public void testTranslatePreGroups() throws Exception {
-
         PiPreEntry multicastGroup = PiReplicationGroupTranslatorImpl
                 .translate(ALL_GROUP, null, null);
         PiPreEntry multicastGroup2 = PiReplicationGroupTranslatorImpl
                 .translate(ALL_GROUP_2, null, null);
         PiPreEntry cloneSessionEntry = PiReplicationGroupTranslatorImpl
                 .translate(CLONE_GROUP, null, null);
+        PiPreEntry cloneSessionEntry2 = PiReplicationGroupTranslatorImpl
+                .translate(CLONE_GROUP_2, null, null);
 
         new EqualsTester()
                 .addEqualityGroup(multicastGroup, PI_MULTICAST_GROUP)
                 .addEqualityGroup(multicastGroup2, PI_MULTICAST_GROUP_2)
                 .addEqualityGroup(cloneSessionEntry, PI_CLONE_SESSION_ENTRY)
+                .addEqualityGroup(cloneSessionEntry2, PI_CLONE_SESSION_ENTRY_2)
                 .testEquals();
     }
+
+    @Test
+    public void testInvalidPreGroups() {
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_ALL_GROUP, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("support only groups with just one OUTPUT instruction per bucket", ex.getMessage());
+        }
+
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_ALL_GROUP_2, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("only CLONE group support truncate instruction", ex.getMessage());
+        }
+
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_ALL_GROUP_3, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("bucket contains unsupported instruction(s)", ex.getMessage());
+        }
+
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_CLONE_GROUP, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("support only groups with just one OUTPUT instruction per bucket", ex.getMessage());
+        }
+
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_CLONE_GROUP_2, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("all TRUNCATE instruction must be the same in a CLONE group", ex.getMessage());
+        }
+
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_CLONE_GROUP_3, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("support only groups with just one TRUNCATE instruction per bucket", ex.getMessage());
+        }
+
+        try {
+            PiReplicationGroupTranslatorImpl
+                    .translate(INVALID_CLONE_GROUP_4, null, null);
+            Assert.fail("Did not get expected exception.");
+        } catch (PiTranslationException ex) {
+            Assert.assertEquals("all TRUNCATE instruction must be the same in a CLONE group", ex.getMessage());
+        }
+    }
 }