Support in fabric pipeliner for pushing double VLAN tag in Next obj

- Small modification to better support pop and route
- To support route and push we expect to receive a Next Objective with two VLAN_ID
- Added capability to check if the pipeline support double VLAN termination

Change-Id: I8bfbf61ccd838a069121e5ab4a804f695a446bac
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/AbstractFabricHandlerBehavior.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/AbstractFabricHandlerBehavior.java
index 2753844..db4da90 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/AbstractFabricHandlerBehavior.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/AbstractFabricHandlerBehavior.java
@@ -30,21 +30,48 @@
 /**
  * Abstract implementation of HandlerBehaviour for the fabric pipeconf
  * behaviors.
+ * <p>
+ * All sub-classes must implement a default constructor, used by the abstract
+ * projectable model (i.e., {@link org.onosproject.net.Device#as(Class)}.
  */
-public class AbstractFabricHandlerBehavior extends AbstractHandlerBehaviour {
+public abstract class AbstractFabricHandlerBehavior extends AbstractHandlerBehaviour {
 
     protected final Logger log = getLogger(getClass());
 
     protected FabricCapabilities capabilities;
 
+    /**
+     * Creates a new instance of this behavior with the given capabilities.
+     * Note: this constructor should be invoked only by other classes of this
+     * package that can retrieve capabilities on their own.
+     * <p>
+     * When using the abstract projectable model (i.e., {@link
+     * org.onosproject.net.Device#as(Class)}, capabilities will be set by the
+     * driver manager when calling {@link #setHandler(DriverHandler)})
+     *
+     * @param capabilities capabilities
+     */
+    protected AbstractFabricHandlerBehavior(FabricCapabilities capabilities) {
+        this.capabilities = capabilities;
+    }
+
+    /**
+     * Create a new instance of this behaviour. Used by the abstract projectable
+     * model (i.e., {@link org.onosproject.net.Device#as(Class)}.
+     */
+    public AbstractFabricHandlerBehavior() {
+        // Do nothing
+    }
+
     @Override
     public void setHandler(DriverHandler handler) {
         super.setHandler(handler);
         final PiPipeconfService pipeconfService = handler().get(PiPipeconfService.class);
-        setCapabilities(handler().data().deviceId(), pipeconfService);
+        setCapabilitiesFromHandler(handler().data().deviceId(), pipeconfService);
     }
 
-    private void setCapabilities(DeviceId deviceId, PiPipeconfService pipeconfService) {
+    private void setCapabilitiesFromHandler(
+            DeviceId deviceId, PiPipeconfService pipeconfService) {
         checkNotNull(deviceId);
         checkNotNull(pipeconfService);
         // Get pipeconf capabilities.
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricCapabilities.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricCapabilities.java
index 07986f9..b0ce208 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricCapabilities.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricCapabilities.java
@@ -76,4 +76,14 @@
             return Optional.empty();
         }
     }
+
+    public boolean supportDoubleVlanTerm() {
+        if (pipeconf.pipelineModel()
+                .table(FabricConstants.FABRIC_INGRESS_NEXT_NEXT_VLAN).isPresent()) {
+            return pipeconf.pipelineModel().table(FabricConstants.FABRIC_INGRESS_NEXT_NEXT_VLAN)
+                    .get().action(FabricConstants.FABRIC_INGRESS_NEXT_SET_DOUBLE_VLAN)
+                    .isPresent();
+        }
+        return false;
+    }
 }
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricIntProgrammable.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricIntProgrammable.java
index d593262..1770576 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricIntProgrammable.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricIntProgrammable.java
@@ -86,6 +86,23 @@
     private DeviceId deviceId;
     private ApplicationId appId;
 
+    /**
+     * Creates a new instance of this behavior with the given capabilities.
+     *
+     * @param capabilities capabilities
+     */
+    protected FabricIntProgrammable(FabricCapabilities capabilities) {
+        super(capabilities);
+    }
+
+    /**
+     * Create a new instance of this behaviour. Used by the abstract projectable
+     * model (i.e., {@link org.onosproject.net.Device#as(Class)}.
+     */
+    public FabricIntProgrammable() {
+        super();
+    }
+
     private boolean setupBehaviour() {
         deviceId = this.data().deviceId();
         flowRuleService = handler().get(FlowRuleService.class);
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java
index 69edeff..8567a3a 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricInterpreter.java
@@ -27,6 +27,7 @@
 import org.onosproject.net.Port;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.DriverHandler;
 import org.onosproject.net.flow.TrafficTreatment;
 import org.onosproject.net.flow.criteria.Criterion;
 import org.onosproject.net.flow.instructions.Instructions;
@@ -116,6 +117,36 @@
                     .put(FabricConstants.FABRIC_INGRESS_FORWARDING_ROUTING_V4, NOP)
                     .build();
 
+    private FabricTreatmentInterpreter treatmentInterpreter;
+
+    /**
+     * Creates a new instance of this behavior with the given capabilities.
+     *
+     * @param capabilities capabilities
+     */
+    public FabricInterpreter(FabricCapabilities capabilities) {
+        super(capabilities);
+        instantiateTreatmentInterpreter();
+    }
+
+    /**
+     * Create a new instance of this behaviour. Used by the abstract projectable
+     * model (i.e., {@link org.onosproject.net.Device#as(Class)}.
+     */
+    public FabricInterpreter() {
+        super();
+    }
+
+    private void instantiateTreatmentInterpreter() {
+        this.treatmentInterpreter = new FabricTreatmentInterpreter(this.capabilities);
+    }
+
+    @Override
+    public void setHandler(DriverHandler handler) {
+        super.setHandler(handler);
+        instantiateTreatmentInterpreter();
+    }
+
     @Override
     public Optional<PiMatchFieldId> mapCriterionType(Criterion.Type type) {
         return Optional.ofNullable(CRITERION_MAP.get(type));
@@ -132,15 +163,15 @@
     public PiAction mapTreatment(TrafficTreatment treatment, PiTableId piTableId)
             throws PiInterpreterException {
         if (FILTERING_CTRL_TBLS.contains(piTableId)) {
-            return FabricTreatmentInterpreter.mapFilteringTreatment(treatment, piTableId);
+            return treatmentInterpreter.mapFilteringTreatment(treatment, piTableId);
         } else if (FORWARDING_CTRL_TBLS.contains(piTableId)) {
-            return FabricTreatmentInterpreter.mapForwardingTreatment(treatment, piTableId);
+            return treatmentInterpreter.mapForwardingTreatment(treatment, piTableId);
         } else if (ACL_CTRL_TBLS.contains(piTableId)) {
-            return FabricTreatmentInterpreter.mapAclTreatment(treatment, piTableId);
+            return treatmentInterpreter.mapAclTreatment(treatment, piTableId);
         } else if (NEXT_CTRL_TBLS.contains(piTableId)) {
-            return FabricTreatmentInterpreter.mapNextTreatment(treatment, piTableId);
+            return treatmentInterpreter.mapNextTreatment(treatment, piTableId);
         } else if (E_NEXT_CTRL_TBLS.contains(piTableId)) {
-            return FabricTreatmentInterpreter.mapEgressNextTreatment(treatment, piTableId);
+            return treatmentInterpreter.mapEgressNextTreatment(treatment, piTableId);
         } else {
             throw new PiInterpreterException(format(
                     "Treatment mapping not supported for table '%s'", piTableId));
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricTreatmentInterpreter.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricTreatmentInterpreter.java
index b7ee4c5..b501f33 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricTreatmentInterpreter.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricTreatmentInterpreter.java
@@ -32,6 +32,9 @@
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionParam;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 import static java.lang.String.format;
 import static org.onosproject.net.flow.instructions.Instruction.Type.OUTPUT;
 import static org.onosproject.net.flow.instructions.L2ModificationInstruction.L2SubType.ETH_DST;
@@ -42,12 +45,14 @@
 import static org.onosproject.net.flow.instructions.L2ModificationInstruction.L2SubType.VLAN_POP;
 import static org.onosproject.pipelines.fabric.FabricUtils.instruction;
 import static org.onosproject.pipelines.fabric.FabricUtils.l2Instruction;
+import static org.onosproject.pipelines.fabric.FabricUtils.l2Instructions;
 
 /**
  * Treatment translation logic.
  */
 final class FabricTreatmentInterpreter {
 
+    private final FabricCapabilities capabilities;
     private static final ImmutableMap<PiTableId, PiActionId> NOP_ACTIONS =
             ImmutableMap.<PiTableId, PiActionId>builder()
                     .put(FabricConstants.FABRIC_INGRESS_FILTERING_INGRESS_PORT_VLAN,
@@ -60,8 +65,9 @@
                          FabricConstants.FABRIC_EGRESS_EGRESS_NEXT_POP_VLAN)
                     .build();
 
-    private FabricTreatmentInterpreter() {
-        // Hide default constructor
+
+    FabricTreatmentInterpreter(FabricCapabilities capabilities) {
+        this.capabilities = capabilities;
     }
 
     static PiAction mapFilteringTreatment(TrafficTreatment treatment, PiTableId tableId)
@@ -73,7 +79,8 @@
             tableException(tableId);
         }
 
-        if (isNoAction(treatment)) {
+        // VLAN_POP action is equivalent to the permit action (VLANs pop is done anyway)
+        if (isNoAction(treatment) || isFilteringPopAction(treatment)) {
             // Permit action if table is ingress_port_vlan;
             return nop(tableId);
         }
@@ -99,7 +106,7 @@
         return null;
     }
 
-    static PiAction mapNextTreatment(TrafficTreatment treatment, PiTableId tableId)
+    PiAction mapNextTreatment(TrafficTreatment treatment, PiTableId tableId)
             throws PiInterpreterException {
         if (tableId == FabricConstants.FABRIC_INGRESS_NEXT_NEXT_VLAN) {
             return mapNextVlanTreatment(treatment, tableId);
@@ -114,16 +121,29 @@
                 "Treatment mapping not supported for table '%s'", tableId));
     }
 
-    private static PiAction mapNextVlanTreatment(TrafficTreatment treatment, PiTableId tableId)
+    private PiAction mapNextVlanTreatment(TrafficTreatment treatment, PiTableId tableId)
             throws PiInterpreterException {
-        final ModVlanIdInstruction modVlanIdInst = (ModVlanIdInstruction)
-                l2InstructionOrFail(treatment, VLAN_ID, tableId);
-        return PiAction.builder()
-                .withId(FabricConstants.FABRIC_INGRESS_NEXT_SET_VLAN)
-                .withParameter(new PiActionParam(
-                        FabricConstants.VLAN_ID,
-                        modVlanIdInst.vlanId().toShort()))
-                .build();
+        final List<ModVlanIdInstruction> modVlanIdInst = l2InstructionsOrFail(treatment, VLAN_ID, tableId)
+                .stream().map(i -> (ModVlanIdInstruction) i).collect(Collectors.toList());
+        if (modVlanIdInst.size() == 1) {
+            return PiAction.builder().withId(FabricConstants.FABRIC_INGRESS_NEXT_SET_VLAN)
+                    .withParameter(new PiActionParam(
+                            FabricConstants.VLAN_ID,
+                            modVlanIdInst.get(0).vlanId().toShort()))
+                    .build();
+        }
+        if (modVlanIdInst.size() == 2 && capabilities.supportDoubleVlanTerm()) {
+            return PiAction.builder()
+                    .withId(FabricConstants.FABRIC_INGRESS_NEXT_SET_DOUBLE_VLAN)
+                    .withParameter(new PiActionParam(
+                            FabricConstants.INNER_VLAN_ID,
+                            modVlanIdInst.get(0).vlanId().toShort()))
+                    .withParameter(new PiActionParam(
+                            FabricConstants.OUTER_VLAN_ID,
+                            modVlanIdInst.get(1).vlanId().toShort()))
+                    .build();
+        }
+        throw new PiInterpreterException("Too many VLAN instructions");
     }
 
     private static PiAction mapNextHashedOrSimpleTreatment(
@@ -223,6 +243,10 @@
                 treatment.allInstructions().isEmpty();
     }
 
+    private static boolean isFilteringPopAction(TrafficTreatment treatment) {
+        return l2Instruction(treatment, VLAN_POP) != null;
+    }
+
     private static Instruction l2InstructionOrFail(
             TrafficTreatment treatment,
             L2ModificationInstruction.L2SubType subType, PiTableId tableId)
@@ -234,6 +258,17 @@
         return inst;
     }
 
+    private static List<L2ModificationInstruction> l2InstructionsOrFail(
+            TrafficTreatment treatment,
+            L2ModificationInstruction.L2SubType subType, PiTableId tableId)
+            throws PiInterpreterException {
+        final List<L2ModificationInstruction> inst = l2Instructions(treatment, subType);
+        if (inst == null || inst.isEmpty()) {
+            treatmentException(tableId, treatment, format("missing %s instruction", subType));
+        }
+        return inst;
+    }
+
     private static Instruction instructionOrFail(
             TrafficTreatment treatment, Instruction.Type type, PiTableId tableId)
             throws PiInterpreterException {
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricUtils.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricUtils.java
index 8e5caa1..ae000b5 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricUtils.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/FabricUtils.java
@@ -27,6 +27,8 @@
 import org.onosproject.net.flowobjective.NextTreatment;
 
 import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.lang.String.format;
@@ -77,6 +79,15 @@
                 .findFirst().orElse(null);
     }
 
+    public static List<L2ModificationInstruction> l2Instructions(
+            TrafficTreatment treatment, L2ModificationInstruction.L2SubType subType) {
+        return treatment.allInstructions().stream()
+                .filter(i -> i.type().equals(Instruction.Type.L2MODIFICATION))
+                .map(i -> (L2ModificationInstruction) i)
+                .filter(i -> i.subtype().equals(subType))
+                .collect(Collectors.toList());
+    }
+
     public static Instructions.OutputInstruction outputInstruction(TrafficTreatment treatment) {
         return instruction(treatment, Instruction.Type.OUTPUT);
     }
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/AbstractObjectiveTranslator.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/AbstractObjectiveTranslator.java
index c1e2f4d..c2ca0ff 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/AbstractObjectiveTranslator.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/AbstractObjectiveTranslator.java
@@ -46,11 +46,12 @@
     protected final FabricCapabilities capabilities;
     protected final DeviceId deviceId;
 
-    private final PiPipelineInterpreter interpreter = new FabricInterpreter();
+    private final PiPipelineInterpreter interpreter;
 
     AbstractObjectiveTranslator(DeviceId deviceId, FabricCapabilities capabilities) {
         this.deviceId = checkNotNull(deviceId);
         this.capabilities = checkNotNull(capabilities);
+        this.interpreter = new FabricInterpreter(capabilities);
     }
 
     public ObjectiveTranslation translate(T obj) {
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricPipeliner.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricPipeliner.java
index 4bf1bb5..20cafde 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricPipeliner.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricPipeliner.java
@@ -38,6 +38,7 @@
 import org.onosproject.net.group.GroupDescription;
 import org.onosproject.net.group.GroupService;
 import org.onosproject.pipelines.fabric.AbstractFabricHandlerBehavior;
+import org.onosproject.pipelines.fabric.FabricCapabilities;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.slf4j.Logger;
 
@@ -78,6 +79,23 @@
 
     private final ExecutorService callbackExecutor = SharedExecutors.getPoolThreadExecutor();
 
+    /**
+     * Creates a new instance of this behavior with the given capabilities.
+     *
+     * @param capabilities capabilities
+     */
+    public FabricPipeliner(FabricCapabilities capabilities) {
+        super(capabilities);
+    }
+
+    /**
+     * Create a new instance of this behaviour. Used by the abstract projectable
+     * model (i.e., {@link org.onosproject.net.Device#as(Class)}.
+     */
+    public FabricPipeliner() {
+        super();
+    }
+
     @Override
     public void init(DeviceId deviceId, PipelinerContext context) {
         this.deviceId = deviceId;
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/NextObjectiveTranslator.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/NextObjectiveTranslator.java
index ec1c64b..d06d383 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/NextObjectiveTranslator.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/NextObjectiveTranslator.java
@@ -61,6 +61,7 @@
 import static org.onosproject.net.flow.instructions.L2ModificationInstruction.L2SubType.VLAN_POP;
 import static org.onosproject.pipelines.fabric.FabricUtils.criterion;
 import static org.onosproject.pipelines.fabric.FabricUtils.l2Instruction;
+import static org.onosproject.pipelines.fabric.FabricUtils.l2Instructions;
 import static org.onosproject.pipelines.fabric.FabricUtils.outputPort;
 
 /**
@@ -112,35 +113,45 @@
     private void nextVlan(NextObjective obj,
                           ObjectiveTranslation.Builder resultBuilder)
             throws FabricPipelinerException {
+        // We expect NextObjective treatments to contain one or two VLAN instructions.
+        // If two, this treatment should be mapped to an action for double-vlan push.
+        // In fabric.p4, mapping next IDs to VLAN IDs is done by a direct table (next_vlan),
+        // for this reason, we also make sure that all treatments in the NextObjective
+        // have exactly the same VLAN instructions, as they will be mapped to a single action
 
-        // Set the egress VLAN for this next ID. Expect a VLAN_ID instruction
-        // in the treatment, or use what's in meta.
-        final List<ModVlanIdInstruction> vlanInstructions = defaultNextTreatments(
+        // Try to extract VLAN instructions in the treatment,
+        //  later we check if we support multiple VLAN termination.
+        final List<List<ModVlanIdInstruction>> vlanInstructions = defaultNextTreatments(
                 obj.nextTreatments(), false).stream()
-                .map(t -> (ModVlanIdInstruction) l2Instruction(t.treatment(), VLAN_ID))
-                .filter(Objects::nonNull)
+                .map(defaultNextTreatment ->
+                             l2Instructions(defaultNextTreatment.treatment(), VLAN_ID)
+                                     .stream().map(v -> (ModVlanIdInstruction) v)
+                                     .collect(Collectors.toList()))
+                .filter(l -> !l.isEmpty())
                 .collect(Collectors.toList());
+
         final VlanIdCriterion vlanIdCriterion = obj.meta() == null ? null
                 : (VlanIdCriterion) criterion(obj.meta().criteria(), Criterion.Type.VLAN_VID);
 
-        VlanId vlanId;
+        final List<VlanId> vlanIdList;
         if (vlanInstructions.isEmpty() && vlanIdCriterion == null) {
             // No VLAN_ID to apply.
             return;
-        } else if (!vlanInstructions.isEmpty()) {
+        }
+        if (!vlanInstructions.isEmpty()) {
             // Give priority to what found in the instructions.
-            // Expect the same VLAN ID for all instructions.
-            final Set<VlanId> vlanIds = vlanInstructions.stream()
-                    .map(ModVlanIdInstruction::vlanId)
+            // Expect the same VLAN ID (or two VLAN IDs in the same order) for all instructions.
+            final Set<List<VlanId>> vlanIds = vlanInstructions.stream()
+                    .map(l -> l.stream().map(ModVlanIdInstruction::vlanId).collect(Collectors.toList()))
                     .collect(Collectors.toSet());
             if (obj.nextTreatments().size() != vlanInstructions.size() ||
                     vlanIds.size() != 1) {
                 throw new FabricPipelinerException(
                         "Inconsistent VLAN_ID instructions, cannot process " +
                                 "next_vlan rule. It is required that all " +
-                                "treatments have the same VLAN_ID instruction.");
+                                "treatments have the same VLAN_ID instructions.");
             }
-            vlanId = vlanIds.iterator().next();
+            vlanIdList = vlanIds.iterator().next();
         } else {
             // Use the value in meta.
             // FIXME: there should be no need to generate a next_vlan rule for
@@ -149,13 +160,13 @@
             //  existing packet fields. But, for some reason, if we remove this
             //  rule, traffic is not forwarded at spines. We might need to look
             //  at the way default VLANs are handled in fabric.p4.
-            vlanId = vlanIdCriterion.vlanId();
+            vlanIdList = List.of(vlanIdCriterion.vlanId());
         }
 
         final TrafficSelector selector = nextIdSelector(obj.id());
-        final TrafficTreatment treatment = DefaultTrafficTreatment.builder()
-                .setVlanId(vlanId)
-                .build();
+        final TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment.builder();
+        vlanIdList.stream().forEach(vlanId -> treatmentBuilder.setVlanId(vlanId));
+        final TrafficTreatment treatment = treatmentBuilder.build();
 
         resultBuilder.addFlowRule(flowRule(
                 obj, FabricConstants.FABRIC_INGRESS_NEXT_NEXT_VLAN,