Support encapsulation in PathIntent

- related to ONOS-3467
- unit test work
- depends on ONOS-3507 and also on the advertisement of VLAN resource
  for different devices.

Change-Id: Ia852c751135b5ca4a16901c6f3a85ceea11514a3
diff --git a/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PathIntentCompiler.java b/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PathIntentCompiler.java
index 7add217..ffced6c 100644
--- a/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PathIntentCompiler.java
+++ b/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PathIntentCompiler.java
@@ -15,42 +15,68 @@
  */
 package org.onosproject.net.intent.impl.compiler;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.packet.VlanId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
 import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.EncapsulationType;
 import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
 import org.onosproject.net.flow.DefaultFlowRule;
 import org.onosproject.net.flow.DefaultTrafficSelector;
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
 import org.onosproject.net.intent.FlowRuleIntent;
 import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentCompiler;
 import org.onosproject.net.intent.IntentExtensionService;
 import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.intent.constraint.EncapsulationConstraint;
+import org.onosproject.net.intent.impl.IntentCompilationException;
+import org.onosproject.net.newresource.ResourcePath;
+import org.onosproject.net.newresource.ResourceService;
 import org.onosproject.net.resource.link.LinkResourceAllocations;
+import org.slf4j.Logger;
 
-import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.onosproject.net.LinkKey.linkKey;
+import static org.slf4j.LoggerFactory.getLogger;
 
 @Component(immediate = true)
 public class PathIntentCompiler implements IntentCompiler<PathIntent> {
 
+    private final Logger log = getLogger(getClass());
+
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected CoreService coreService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected IntentExtensionService intentManager;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ResourceService resourceService;
+
     private ApplicationId appId;
 
     @Activate
@@ -71,29 +97,47 @@
         // TODO: implement recompile behavior
 
         List<Link> links = intent.path().links();
-        List<FlowRule> rules = new ArrayList<>(links.size() - 1);
+        List<FlowRule> rules = new LinkedList<>();
 
-        for (int i = 0; i < links.size() - 1; i++) {
-            ConnectPoint ingress = links.get(i).dst();
-            ConnectPoint egress = links.get(i + 1).src();
-            FlowRule rule = createFlowRule(intent.selector(), intent.treatment(),
-                                           ingress, egress, intent.priority(),
-                                           isLast(links, i));
-            rules.add(rule);
+        Optional<EncapsulationConstraint> enacpConstraint = intent.constraints().stream()
+                .filter(constraint -> constraint instanceof EncapsulationConstraint)
+                .map(x -> (EncapsulationConstraint) x).findAny();
+        //if no encapsulation or is involved only a single switch use the default behaviour
+        if (!enacpConstraint.isPresent() || links.size() == 1) {
+
+            for (int i = 0; i < links.size() - 1; i++) {
+                ConnectPoint ingress = links.get(i).dst();
+                ConnectPoint egress = links.get(i + 1).src();
+                FlowRule rule = createFlowRule(intent.selector(), intent.treatment(),
+                                               ingress, egress, intent.priority(),
+                                               isLast(links, i));
+                rules.add(rule);
+            }
+
+            return ImmutableList.of(new FlowRuleIntent(appId, null, rules, intent.resources()));
+
+        } else {
+            if (EncapsulationType.VLAN == enacpConstraint.get().encapType()) {
+                rules = manageVlanEncap(intent);
+            }
+            if (EncapsulationType.MPLS == enacpConstraint.get().encapType()) {
+                //TODO: to be implemented
+                rules = Collections.emptyList();
+            }
+
+            return ImmutableList.of(new FlowRuleIntent(appId, null, rules, intent.resources()));
         }
-
-        return Collections.singletonList(new FlowRuleIntent(appId, null, rules, intent.resources()));
     }
 
     private FlowRule createFlowRule(TrafficSelector originalSelector, TrafficTreatment originalTreatment,
                                     ConnectPoint ingress, ConnectPoint egress,
-                                    int priority, boolean last) {
+                                    int priority, boolean applyTreatment) {
         TrafficSelector selector = DefaultTrafficSelector.builder(originalSelector)
                 .matchInPort(ingress.port())
                 .build();
 
         TrafficTreatment.Builder treatmentBuilder;
-        if (last) {
+        if (applyTreatment) {
             treatmentBuilder = DefaultTrafficTreatment.builder(originalTreatment);
         } else {
             treatmentBuilder = DefaultTrafficTreatment.builder();
@@ -110,6 +154,138 @@
                 .build();
     }
 
+    private List<FlowRule> manageVlanEncap(PathIntent intent) {
+        Map<LinkKey, VlanId> vlanIds = assignVlanId(intent);
+
+        Iterator<Link> links = intent.path().links().iterator();
+        Link srcLink = links.next();
+
+        Link link = links.next();
+        // List of flow rules to be installed
+        List<FlowRule> rules = new LinkedList<>();
+
+        // Ingress traffic
+        VlanId vlanId = vlanIds.get(linkKey(link));
+        if (vlanId == null) {
+            throw new IntentCompilationException("No available VLAN ID for " + link);
+        }
+        VlanId prevVlanId = vlanId;
+
+        //Tag the traffic with the new VLAN
+        TrafficTreatment treat = DefaultTrafficTreatment.builder()
+                .setVlanId(vlanId)
+                .build();
+
+        rules.add(createFlowRule(intent.selector(), treat, srcLink.dst(), link.src(), intent.priority(), true));
+
+        ConnectPoint prev = link.dst();
+
+        while (links.hasNext()) {
+
+            link = links.next();
+
+            if (links.hasNext()) {
+                // Transit traffic
+                VlanId egressVlanId = vlanIds.get(linkKey(link));
+                if (egressVlanId == null) {
+                    throw new IntentCompilationException("No available VLAN ID for " + link);
+                }
+                prevVlanId = egressVlanId;
+
+                TrafficSelector transitSelector = DefaultTrafficSelector.builder()
+                        .matchInPort(prev.port())
+                        .matchVlanId(prevVlanId).build();
+
+                TrafficTreatment.Builder transitTreat = DefaultTrafficTreatment.builder();
+
+                // Set the new vlanId only if the previous one is different
+                if (!prevVlanId.equals(egressVlanId)) {
+                    transitTreat.setVlanId(egressVlanId);
+                }
+                rules.add(createFlowRule(transitSelector,
+                                         transitTreat.build(), prev, link.src(), intent.priority(), true));
+                prev = link.dst();
+            } else {
+                // Egress traffic
+                TrafficSelector egressSelector = DefaultTrafficSelector.builder()
+                        .matchInPort(prev.port())
+                        .matchVlanId(prevVlanId).build();
+
+                //TODO: think to other cases for egress packet restoration
+                Optional<VlanIdCriterion> vlanCriteria = intent.selector().criteria()
+                        .stream().filter(criteria -> criteria.type() == Criterion.Type.VLAN_VID)
+                        .map(criteria -> (VlanIdCriterion) criteria)
+                        .findAny();
+                TrafficTreatment.Builder egressTreat = DefaultTrafficTreatment.builder(intent.treatment());
+                if (vlanCriteria.isPresent()) {
+                    egressTreat.setVlanId(vlanCriteria.get().vlanId());
+                } else {
+                    egressTreat.popVlan();
+                }
+
+                rules.add(createFlowRule(egressSelector,
+                                         egressTreat.build(), prev, link.src(), intent.priority(), true));
+            }
+
+        }
+        return rules;
+
+    }
+
+    private Map<LinkKey, VlanId> assignVlanId(PathIntent intent) {
+        Set<LinkKey> linkRequest = Sets.newHashSetWithExpectedSize(intent.path()
+                                                                           .links().size() - 2);
+        for (int i = 1; i <= intent.path().links().size() - 2; i++) {
+            LinkKey link = linkKey(intent.path().links().get(i));
+            linkRequest.add(link);
+            // add the inverse link. I want that the VLANID is reserved both for
+            // the direct and inverse link
+            linkRequest.add(linkKey(link.dst(), link.src()));
+        }
+
+        Map<LinkKey, VlanId> vlanIds = findVlanIds(linkRequest);
+        if (vlanIds.isEmpty()) {
+            log.warn("No VLAN IDs available");
+            return Collections.emptyMap();
+        }
+
+        //same VLANID is used for both directions
+        Set<ResourcePath> resources = vlanIds.entrySet().stream()
+                .flatMap(x -> Stream.of(
+                        ResourcePath.discrete(x.getKey().src().deviceId(), x.getKey().src().port(), x.getValue()),
+                        ResourcePath.discrete(x.getKey().dst().deviceId(), x.getKey().dst().port(), x.getValue())
+                ))
+                .collect(Collectors.toSet());
+        List<org.onosproject.net.newresource.ResourceAllocation> allocations =
+                resourceService.allocate(intent.id(), ImmutableList.copyOf(resources));
+        if (allocations.isEmpty()) {
+            Collections.emptyMap();
+        }
+
+        return vlanIds;
+    }
+
+    private Map<LinkKey, VlanId> findVlanIds(Set<LinkKey> links) {
+        Map<LinkKey, VlanId> vlanIds = new HashMap<>();
+        for (LinkKey link : links) {
+            Set<VlanId> forward = findVlanId(link.src());
+            Set<VlanId> backward = findVlanId(link.dst());
+            Set<VlanId> common = Sets.intersection(forward, backward);
+            if (common.isEmpty()) {
+                continue;
+            }
+            vlanIds.put(link, common.iterator().next());
+        }
+        return vlanIds;
+    }
+
+    private Set<VlanId> findVlanId(ConnectPoint cp) {
+        return resourceService.getAvailableResources(ResourcePath.discrete(cp.deviceId(), cp.port())).stream()
+                .filter(x -> x.last() instanceof VlanId)
+                .map(x -> (VlanId) x.last())
+                .collect(Collectors.toSet());
+    }
+
     private boolean isLast(List<Link> links, int i) {
         return i == links.size() - 2;
     }
diff --git a/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/MockResourceService.java b/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/MockResourceService.java
index 2926346..866c513 100644
--- a/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/MockResourceService.java
+++ b/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/MockResourceService.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.ImmutableList;
 import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
 import org.onosproject.net.newresource.ResourceAllocation;
 import org.onosproject.net.newresource.ResourceConsumer;
 import org.onosproject.net.newresource.ResourceListener;
@@ -25,6 +26,7 @@
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -91,8 +93,11 @@
 
     @Override
     public Collection<ResourcePath> getAvailableResources(ResourcePath parent) {
-        ResourcePath resource = parent.child(MplsLabel.mplsLabel(10));
-        return ImmutableList.of(resource);
+
+        Collection<ResourcePath> resources = new HashSet<ResourcePath>();
+        resources.add(parent.child(VlanId.vlanId((short) 10)));
+        resources.add(parent.child(MplsLabel.mplsLabel(10)));
+        return ImmutableList.copyOf(resources);
     }
 
     @Override
diff --git a/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/PathIntentCompilerTest.java b/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/PathIntentCompilerTest.java
index f07bf42..dda82c6 100644
--- a/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/PathIntentCompilerTest.java
+++ b/core/net/src/test/java/org/onosproject/net/intent/impl/compiler/PathIntentCompilerTest.java
@@ -15,14 +15,11 @@
  */
 package org.onosproject.net.intent.impl.compiler;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
+import com.google.common.collect.ImmutableList;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.onlab.packet.VlanId;
 import org.onosproject.TestApplicationId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
@@ -30,30 +27,38 @@
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DefaultLink;
 import org.onosproject.net.DefaultPath;
+import org.onosproject.net.EncapsulationType;
 import org.onosproject.net.Link;
 import org.onosproject.net.flow.DefaultTrafficSelector;
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.instructions.L2ModificationInstruction;
 import org.onosproject.net.intent.FlowRuleIntent;
 import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentExtensionService;
 import org.onosproject.net.intent.MockIdGenerator;
 import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.intent.constraint.EncapsulationConstraint;
 import org.onosproject.net.provider.ProviderId;
 
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.easymock.EasyMock.*;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.number.OrderingComparison.greaterThan;
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
 import static org.onosproject.net.Link.Type.DIRECT;
-import static org.onosproject.net.NetTestTools.APP_ID;
-import static org.onosproject.net.NetTestTools.PID;
-import static org.onosproject.net.NetTestTools.connectPoint;
+import static org.onosproject.net.NetTestTools.*;
 
 /**
  * Unit tests for PathIntentCompiler.
@@ -85,6 +90,7 @@
     );
     private final int hops = links.size() - 1;
     private PathIntent intent;
+    private PathIntent constraintIntent;
 
     /**
      * Configures objects used in all the test cases.
@@ -96,6 +102,7 @@
         expect(coreService.registerApplication("org.onosproject.net.intent"))
                 .andReturn(appId);
         sut.coreService = coreService;
+        sut.resourceService = new MockResourceService();
 
         Intent.bindIdGenerator(idGenerator);
 
@@ -106,6 +113,14 @@
                 .priority(PRIORITY)
                 .path(new DefaultPath(pid, links, hops))
                 .build();
+        constraintIntent = PathIntent.builder()
+                .appId(APP_ID)
+                .selector(selector)
+                .treatment(treatment)
+                .priority(PRIORITY)
+                .constraints(ImmutableList.of(new EncapsulationConstraint(EncapsulationType.VLAN)))
+                .path(new DefaultPath(pid, links, hops))
+                .build();
         intentExtensionService = createMock(IntentExtensionService.class);
         intentExtensionService.registerCompiler(PathIntent.class, sut);
         intentExtensionService.unregisterCompiler(PathIntent.class);
@@ -169,4 +184,91 @@
 
         sut.deactivate();
     }
+
+    /**
+     * Tests the compilation behavior of the path intent compiler in case of
+     * encasulation costraint {@link EncapsulationConstraint}.
+     */
+    @Test
+    public void testEncapCompile() {
+        sut.activate();
+
+        List<Intent> compiled = sut.compile(constraintIntent, Collections.emptyList(), Collections.emptySet());
+        assertThat(compiled, hasSize(1));
+
+        Collection<FlowRule> rules = ((FlowRuleIntent) compiled.get(0)).flowRules();
+        assertThat(rules, hasSize(3));
+
+        FlowRule rule1 = rules.stream()
+                .filter(x -> x.deviceId().equals(d1p0.deviceId()))
+                .findFirst()
+                .get();
+        assertThat(rule1.deviceId(), is(d1p0.deviceId()));
+        assertThat(rule1.priority(), is(intent.priority()));
+        verifyEncapSelector(rule1.selector(), d1p0, VlanId.NONE);
+        VlanId vlanToEncap = verifyEncapTreatment(rule1.treatment(), d1p1, true, false);
+
+        FlowRule rule2 = rules.stream()
+                .filter(x -> x.deviceId().equals(d2p0.deviceId()))
+                .findFirst()
+                .get();
+        assertThat(rule2.deviceId(), is(d2p0.deviceId()));
+        assertThat(rule2.priority(), is(intent.priority()));
+        verifyEncapSelector(rule2.selector(), d2p0, vlanToEncap);
+        verifyEncapTreatment(rule2.treatment(), d2p1, false, false);
+
+        FlowRule rule3 = rules.stream()
+                .filter(x -> x.deviceId().equals(d3p0.deviceId()))
+                .findFirst()
+                .get();
+        assertThat(rule3.deviceId(), is(d3p1.deviceId()));
+        assertThat(rule3.priority(), is(intent.priority()));
+        verifyEncapSelector(rule3.selector(), d3p1, vlanToEncap);
+        verifyEncapTreatment(rule3.treatment(), d3p0, false, true);
+
+        sut.deactivate();
+    }
+
+
+    private VlanId verifyEncapTreatment(TrafficTreatment trafficTreatment,
+                                        ConnectPoint egress, boolean isIngress, boolean isEgress) {
+        Set<Instructions.OutputInstruction> ruleOutput = trafficTreatment.allInstructions().stream()
+                .filter(treat -> treat instanceof Instructions.OutputInstruction)
+                .map(treat -> (Instructions.OutputInstruction) treat)
+                .collect(Collectors.toSet());
+        assertThat(ruleOutput, hasSize(1));
+        assertThat((ruleOutput.iterator().next()).port(), is(egress.port()));
+        VlanId vlanToEncap = VlanId.NONE;
+        if (isIngress && !isEgress) {
+            Set<L2ModificationInstruction.ModVlanIdInstruction> vlanRules = trafficTreatment.allInstructions().stream()
+                    .filter(treat -> treat instanceof L2ModificationInstruction.ModVlanIdInstruction)
+                    .map(x -> (L2ModificationInstruction.ModVlanIdInstruction) x)
+                    .collect(Collectors.toSet());
+            assertThat(vlanRules, hasSize(1));
+            L2ModificationInstruction.ModVlanIdInstruction vlanRule = vlanRules.iterator().next();
+            assertThat(vlanRule.vlanId().toShort(), greaterThan((short) 0));
+            vlanToEncap = vlanRule.vlanId();
+        } else if (!isIngress && !isEgress) {
+            assertThat(trafficTreatment.allInstructions().stream()
+                               .filter(treat -> treat instanceof L2ModificationInstruction.ModVlanIdInstruction)
+                               .collect(Collectors.toSet()), hasSize(0));
+        } else {
+            assertThat(trafficTreatment.allInstructions().stream()
+                               .filter(treat -> treat instanceof L2ModificationInstruction.ModVlanIdInstruction)
+                               .collect(Collectors.toSet()), hasSize(0));
+            assertThat(trafficTreatment.allInstructions().stream()
+                               .filter(treat -> treat instanceof L2ModificationInstruction.PopVlanInstruction)
+                               .collect(Collectors.toSet()), hasSize(1));
+
+        }
+
+        return vlanToEncap;
+
+    }
+
+    private void verifyEncapSelector(TrafficSelector trafficSelector, ConnectPoint ingress, VlanId vlanToMatch) {
+
+        is(DefaultTrafficSelector.builder(selector).matchInPort(ingress.port())
+                   .matchVlanId(vlanToMatch).build());
+    }
 }