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
diff --git a/core/api/src/main/java/org/onosproject/net/flow/DefaultTrafficTreatment.java b/core/api/src/main/java/org/onosproject/net/flow/DefaultTrafficTreatment.java
index ffad4f7..467cc6d 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/DefaultTrafficTreatment.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/DefaultTrafficTreatment.java
@@ -273,6 +273,7 @@
                 case L4MODIFICATION:
                 case PROTOCOL_INDEPENDENT:
                 case EXTENSION:
+                case TRUNCATE:
                     current.add(instruction);
                     break;
                 case TABLE:
@@ -559,6 +560,11 @@
         }
 
         @Override
+        public TrafficTreatment.Builder truncate(int maxLen) {
+            return add(Instructions.truncate(maxLen));
+        }
+
+        @Override
         public TrafficTreatment build() {
             if (deferred.isEmpty() && immediate.isEmpty()
                     && table == null && !clear) {
diff --git a/core/api/src/main/java/org/onosproject/net/flow/TrafficTreatment.java b/core/api/src/main/java/org/onosproject/net/flow/TrafficTreatment.java
index 9fde774..538a78f 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/TrafficTreatment.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/TrafficTreatment.java
@@ -480,6 +480,14 @@
                             StatTriggerFlag statTriggerFlag);
 
         /**
+         * Adds a truncate instruction.
+         *
+         * @param maxLen the maximum frame length in bytes, must be a positive integer
+         * @return a treatment builder
+         */
+        Builder truncate(int maxLen);
+
+        /**
          * Add all instructions from another treatment.
          *
          * @param treatment another treatment
diff --git a/core/api/src/main/java/org/onosproject/net/flow/instructions/Instruction.java b/core/api/src/main/java/org/onosproject/net/flow/instructions/Instruction.java
index 66fbb18..a87f3a3 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/instructions/Instruction.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/instructions/Instruction.java
@@ -101,7 +101,12 @@
         /**
          * Signifies that statistics will be triggered.
          */
-        STAT_TRIGGER
+        STAT_TRIGGER,
+
+        /**
+         * Signifies that the packet should be truncated.
+         */
+        TRUNCATE
     }
 
     /**
diff --git a/core/api/src/main/java/org/onosproject/net/flow/instructions/Instructions.java b/core/api/src/main/java/org/onosproject/net/flow/instructions/Instructions.java
index 621c007..d330dcd 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/instructions/Instructions.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/instructions/Instructions.java
@@ -49,6 +49,7 @@
 import java.util.Objects;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
@@ -545,6 +546,17 @@
     }
 
     /**
+     * Creates a truncate instruction.
+     *
+     * @param maxLen the maximum frame length in bytes, must be a positive integer
+     * @return truncate instruction
+     */
+    public static TruncateInstruction truncate(int maxLen) {
+        checkArgument(maxLen > 0, "Truncate max length must be a positive integer.");
+        return new TruncateInstruction(maxLen);
+    }
+
+    /**
      *  No Action instruction.
      */
     public static final class NoActionInstruction implements Instruction {
@@ -974,6 +986,44 @@
         }
     }
 
+    public static final class TruncateInstruction implements Instruction {
+        private int maxLen;
+
+        public TruncateInstruction(int maxLen) {
+            this.maxLen = maxLen;
+        }
+
+        public int maxLen() {
+            return maxLen;
+        }
+
+        @Override
+        public Type type() {
+            return Type.TRUNCATE;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            TruncateInstruction that = (TruncateInstruction) o;
+            return maxLen == that.maxLen;
+        }
+
+        @Override
+        public int hashCode() {
+            return com.google.common.base.Objects.hashCode(maxLen);
+        }
+
+        @Override
+        public String toString() {
+            return type() + SEPARATOR + maxLen;
+        }
+    }
 }
 
 
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());
+        }
+    }
 }