ONOS-5603 ProtectedTransportIntentCompiler

Change-Id: I681f24662d8e9be06f1e216fa9aa45b1dd44757d
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectedTransportEndpointDescription.java b/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectedTransportEndpointDescription.java
index 2e563de..dcdd895 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectedTransportEndpointDescription.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectedTransportEndpointDescription.java
@@ -113,14 +113,29 @@
      * Creates a {@link ProtectedTransportEndpointDescription}.
      *
      * @param paths {@link TransportEndpointDescription}s forming protection
-     * @param did DeviceId of remote peer of this endpoint.
+     * @param peer DeviceId of remote peer of this endpoint.
+     * @param fingerprint opaque fingerprint object. must be serializable.
+     * @return {@link TransportEndpointDescription}
+     */
+    public static final ProtectedTransportEndpointDescription
+            buildDescription(List<TransportEndpointDescription> paths,
+                             DeviceId peer,
+                             String fingerprint) {
+        return new ProtectedTransportEndpointDescription(paths, peer, fingerprint);
+    }
+
+    /**
+     * Creates a {@link ProtectedTransportEndpointDescription}.
+     *
+     * @param paths {@link TransportEndpointDescription}s forming protection
+     * @param peer DeviceId of remote peer of this endpoint.
      * @param fingerprint opaque fingerprint object. must be serializable.
      * @return {@link TransportEndpointDescription}
      */
     public static final ProtectedTransportEndpointDescription
                             of(List<TransportEndpointDescription> paths,
-                               DeviceId did,
+                               DeviceId peer,
                                String fingerprint) {
-        return new ProtectedTransportEndpointDescription(paths, did, fingerprint);
+        return new ProtectedTransportEndpointDescription(paths, peer, fingerprint);
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectionConfig.java b/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectionConfig.java
index 1bcafba..085bc1f 100644
--- a/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectionConfig.java
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/protection/ProtectionConfig.java
@@ -16,6 +16,7 @@
 package org.onosproject.net.behaviour.protection;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.behaviour.protection.ProtectedTransportEndpointDescription.buildDescription;
 
 import java.util.List;
 
@@ -127,7 +128,7 @@
      * @return {@link ProtectedTransportEndpointDescription}
      */
     public ProtectedTransportEndpointDescription asDescription() {
-        return ProtectedTransportEndpointDescription.of(paths(), peer(), fingerprint());
+        return buildDescription(paths(), peer(), fingerprint());
     }
 
     @Override
diff --git a/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/ProtectedTransportIntentCompiler.java b/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/ProtectedTransportIntentCompiler.java
new file mode 100644
index 0000000..52c6dee
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/ProtectedTransportIntentCompiler.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.net.intent.impl.compiler;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Lists.transform;
+import static java.util.stream.Stream.concat;
+import static org.onosproject.net.behaviour.protection.ProtectedTransportEndpointDescription.buildDescription;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.commons.lang3.tuple.Pair;
+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.net.ConnectPoint;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DefaultPath;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.DisjointPath;
+import org.onosproject.net.FilteredConnectPoint;
+import org.onosproject.net.Link;
+import org.onosproject.net.Link.State;
+import org.onosproject.net.NetworkResource;
+import org.onosproject.net.Path;
+import org.onosproject.net.behaviour.protection.TransportEndpointDescription;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentCompilationException;
+import org.onosproject.net.intent.LinkCollectionIntent;
+import org.onosproject.net.intent.ProtectedTransportIntent;
+import org.onosproject.net.intent.ProtectionEndpointIntent;
+import org.onosproject.net.resource.DiscreteResourceId;
+import org.onosproject.net.resource.Resource;
+import org.onosproject.net.resource.ResourceService;
+import org.onosproject.net.resource.Resources;
+import org.slf4j.Logger;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+/**
+ * IntentCompiler for {@link ProtectedTransportIntent}.
+ */
+@Beta
+@Component(immediate = true)
+public class ProtectedTransportIntentCompiler
+        extends ConnectivityIntentCompiler<ProtectedTransportIntent> {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ResourceService resourceService;
+
+    @Activate
+    public void activate() {
+        intentManager.registerCompiler(ProtectedTransportIntent.class, this);
+        log.info("started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        intentManager.unregisterCompiler(ProtectedTransportIntent.class);
+        log.info("stopped");
+    }
+
+    @Override
+    public List<Intent> compile(ProtectedTransportIntent intent,
+                                List<Intent> installable) {
+        log.trace("compiling {} {}", intent, installable);
+
+        // case 0 hop, same device
+        final DeviceId did1 = intent.one();
+        final DeviceId did2 = intent.two();
+        if (Objects.equals(did1, did2)) {
+            // Doesn't really make sense to create 0 hop protected path, but
+            // can generate Flow for the device, just to provide connectivity.
+            // future work.
+            log.error("0 hop not supported yet.");
+            throw new IntentCompilationException("0 hop not supported yet.");
+        }
+
+        List<Intent> reusable = Optional.ofNullable(installable).orElse(ImmutableList.of())
+            .stream()
+            .filter(this::isIntact)
+            .collect(Collectors.toList());
+        if (reusable.isEmpty() ||
+            reusable.stream().allMatch(ProtectionEndpointIntent.class::isInstance)) {
+            // case provisioning new protected path
+            //   or
+            // case re-compilation (total failure -> restoration)
+            return createFreshProtectedPaths(intent, did1, did2);
+        } else {
+            // case re-compilation (partial failure)
+            log.warn("Re-computing adding new backup path not supported yet. No-Op.");
+            // TODO This part needs to be flexible to support various use case
+            // - non-revertive behavior (Similar to PartialFailureConstraint)
+            // - revertive behavior
+            // - compute third path
+            // ...
+            //  Require further input what they actually need.
+
+            // TODO handle PartialFailureConstraint
+
+            /// case only need to update transit portion
+            /// case head and/or tail needs to be updated
+
+            // TODO do we need to prune broken
+            return installable;
+        }
+    }
+
+    /**
+     * Test if resources used by specified Intent is intact.
+     *
+     * @param installed Intent to test
+     * @return true if Intent is intact
+     */
+    private boolean isIntact(Intent installed) {
+        return installed.resources().stream()
+            .filter(Link.class::isInstance)
+            .map(Link.class::cast)
+            .allMatch(this::isLive);
+    }
+
+    /**
+     * Test if specified Link is intact.
+     *
+     * @param link to test
+     * @return true if link is intact
+     */
+    private boolean isLive(Link link) {
+        // Only testing link state for now
+        // in the long run, consider verifying OAM state on ports
+        return link.state() != State.INACTIVE;
+    }
+
+    /**
+     * Creates new protected paths.
+     *
+     * @param intent    original intention
+     * @param did1      identifier of first device
+     * @param did2      identifier of second device
+     * @return compilation result
+     * @throws IntentCompilationException when there's no satisfying path.
+     */
+    private List<Intent> createFreshProtectedPaths(ProtectedTransportIntent intent,
+                                                   DeviceId did1,
+                                                   DeviceId did2) {
+        DisjointPath disjointPath = getDisjointPath(intent, did1, did2);
+        if (disjointPath == null || disjointPath.backup() == null) {
+            log.error("Unable to find disjoint path between {}, {}", did1, did2);
+            throw new IntentCompilationException("Unable to find disjoint paths.");
+        }
+        Path primary = disjointPath.primary();
+        Path secondary = disjointPath.backup();
+
+        String fingerprint = intent.key().toString();
+
+        // pick and allocate Vlan to use as S-tag
+        Pair<VlanId, VlanId> vlans = allocateEach(intent, primary, secondary, VlanId.class);
+
+        VlanId primaryVlan = vlans.getLeft();
+        VlanId secondaryVlan = vlans.getRight();
+
+        // Build edge Intents for head/tail
+
+        // resource for head/tail
+        Collection<NetworkResource> oneResources = new ArrayList<>();
+        Collection<NetworkResource> twoResources = new ArrayList<>();
+
+        List<TransportEndpointDescription> onePaths = new ArrayList<>();
+        onePaths.add(TransportEndpointDescription.builder()
+                         .withOutput(vlanPort(primary.src(), primaryVlan))
+                         .build());
+        onePaths.add(TransportEndpointDescription.builder()
+                         .withOutput(vlanPort(secondary.src(), secondaryVlan))
+                         .build());
+
+        List<TransportEndpointDescription> twoPaths = new ArrayList<>();
+        twoPaths.add(TransportEndpointDescription.builder()
+                     .withOutput(vlanPort(primary.dst(), primaryVlan))
+                     .build());
+        twoPaths.add(TransportEndpointDescription.builder()
+                     .withOutput(vlanPort(secondary.dst(), secondaryVlan))
+                     .build());
+
+        ProtectionEndpointIntent oneIntent = ProtectionEndpointIntent.builder()
+                .key(intent.key())
+                .appId(intent.appId())
+                .priority(intent.priority())
+                .resources(oneResources)
+                .deviceId(did1)
+                .description(buildDescription(onePaths, did2, fingerprint))
+                .build();
+        ProtectionEndpointIntent twoIntent = ProtectionEndpointIntent.builder()
+                .key(intent.key())
+                .appId(intent.appId())
+                .resources(twoResources)
+                .deviceId(did2)
+                .description(buildDescription(twoPaths, did1, fingerprint))
+                .build();
+
+        // Build transit intent for primary/secondary path
+
+        ImmutableList<Intent> result = ImmutableList.<Intent>builder()
+                // LinkCollection for primary and backup paths
+                .addAll(createTransitIntent(intent, primary, primaryVlan))
+                .addAll(createTransitIntent(intent, secondary, secondaryVlan))
+                .add(oneIntent)
+                .add(twoIntent)
+                .build();
+        log.trace("createFreshProtectedPaths result: {}", result);
+        return result;
+    }
+
+    /**
+     * Creates required Intents required to transit bi-directionally the network.
+     *
+     * @param intent parent IntentId
+     * @param path whole path
+     * @param vid VlanId to use as tunnel labels
+     * @return List on transit Intents, if any is required.
+     */
+    List<LinkCollectionIntent> createTransitIntent(Intent intent, Path path, VlanId vid) {
+        if (path.links().size() <= 1) {
+            // There's no need for transit Intents
+            return ImmutableList.of();
+        }
+
+        return ImmutableList.of(createSubTransitIntent(intent, path, vid),
+                                createSubTransitIntent(intent, reverse(path), vid));
+    }
+
+    /**
+     * Returns a path in reverse direction.
+     *
+     * @param path to reverse
+     * @return reversed path
+     */
+    Path reverse(Path path) {
+        List<Link> revLinks = Lists.reverse(transform(path.links(), this::reverse));
+        return new DefaultPath(path.providerId(),
+                               revLinks,
+                               path.cost(),
+                               path.annotations());
+    }
+
+    // TODO consider adding equivalent to Link/DefaultLink.
+    /**
+     * Returns a link in reverse direction.
+     *
+     * @param link to revese
+     * @return reversed link
+     */
+    Link reverse(Link link) {
+        return DefaultLink.builder()
+                .providerId(link.providerId())
+                .src(link.dst())
+                .dst(link.src())
+                .type(link.type())
+                .state(link.state())
+                .isExpected(link.isExpected())
+                .annotations(link.annotations())
+                .build();
+    }
+
+    /**
+     * Creates required Intents required to transit uni-directionally along the Path.
+     *
+     * @param intent parent IntentId
+     * @param path whole path
+     * @param vid VlanId to use as tunnel labels
+     * @return List on transit Intents, if any is required.
+     */
+    LinkCollectionIntent createSubTransitIntent(Intent intent, Path path, VlanId vid) {
+        checkArgument(path.links().size() > 1);
+
+        // transit ingress/egress
+        ConnectPoint one = path.links().get(0).dst();
+        ConnectPoint two = path.links().get(path.links().size() - 1).src();
+
+        return LinkCollectionIntent.builder()
+                    // TODO there should probably be .parent(intent)
+                    // which copies key, appId, priority, ...
+                    .key(intent.key())
+                    .appId(intent.appId())
+                    .priority(intent.priority())
+                    //.constraints(intent.constraints())
+                    // VLAN tunnel
+                    //.selector(DefaultTrafficSelector.builder().matchVlanId(vid).build())
+                    //.treatment(intent.treatment())
+                    .links(ImmutableSet.copyOf(path.links()))
+                    .filteredIngressPoints(ImmutableSet.of(vlanPort(one, vid)))
+                    .filteredEgressPoints(ImmutableSet.of(vlanPort(two, vid)))
+                    // magic flag required for p2p type
+                    .applyTreatmentOnEgress(true)
+                    .cost(path.cost())
+                    .build();
+    }
+
+    /**
+     * Creates VLAN filtered-ConnectPoint.
+     *
+     * @param cp  ConnectPoint
+     * @param vid VLAN ID
+     * @return filtered-ConnectPoint
+     */
+    static FilteredConnectPoint vlanPort(ConnectPoint cp, VlanId vid) {
+        return new FilteredConnectPoint(cp, DefaultTrafficSelector.builder()
+                                        .matchVlanId(vid)
+                                        .build());
+    }
+
+    /**
+     * Creates ResourceId for a port.
+     *
+     * @param cp ConnectPoint
+     * @return ResourceId
+     */
+    static DiscreteResourceId resourceId(ConnectPoint cp) {
+        return Resources.discrete(cp.deviceId(), cp.port()).id();
+    }
+
+    /**
+     * Allocate resource for each {@link Path}s.
+     *
+     * @param intent to allocate resource to
+     * @param primary path
+     * @param secondary path
+     * @param klass label resource class
+     * @return Pair of chosen resource (primary, secondary)
+     * @param <T> label resource type
+     * @throws IntentCompilationException when there is no resource available
+     */
+    <T> Pair<T, T> allocateEach(Intent intent, Path primary, Path secondary, Class<T> klass) {
+        log.trace("allocateEach({}, {}, {}, {})", intent, primary, secondary, klass);
+        Pair<T, T> vlans = null;
+        do {
+            Set<T> primaryVlans = commonLabelResource(primary, klass);
+            Set<T> secondaryVlans = commonLabelResource(secondary, klass);
+            Pair<T, T> candidates = pickEach(primaryVlans, secondaryVlans);
+            T primaryT = candidates.getLeft();
+            T secondaryT = candidates.getRight();
+
+            // try to allocate candidates along each path
+            Stream<Resource> primaryResources = primary.links().stream()
+                    .flatMap(link -> Stream.of(link.src(), link.dst()))
+                    .distinct()
+                    .map(cp -> Resources.discrete(resourceId(cp), primaryT).resource());
+            Stream<Resource> secondaryResources = secondary.links().stream()
+                    .flatMap(link -> Stream.of(link.src(), link.dst()))
+                    .distinct()
+                    .map(cp -> Resources.discrete(resourceId(cp), secondaryT).resource());
+
+            List<Resource> resources = concat(primaryResources, secondaryResources)
+                                        .collect(Collectors.toList());
+            log.trace("Calling allocate({},{})", intent.id(), resources);
+            if (resourceService.allocate(intent.id(), resources).isEmpty()) {
+                log.warn("Allocation failed, retrying");
+                continue;
+            }
+            vlans = candidates;
+        } while (false);
+        log.trace("allocation done.");
+        return vlans;
+    }
+
+    /**
+     * Randomly pick one resource from candidates.
+     *
+     * @param set of candidates
+     * @return chosen one
+     * @param <T> label resource type
+     */
+    <T> T pickOne(Set<T> set) {
+        // Note: Set returned by commonLabelResource(..) assures,
+        // there is at least one element.
+
+        // FIXME more reasonable selection logic
+        return Iterables.get(set, RandomUtils.nextInt(0, set.size()));
+    }
+
+    /**
+     * Select resource from available Resources.
+     *
+     * @param primary   Set of resource to pick from
+     * @param secondary Set of resource to pick from
+     * @return Pair of chosen resource (primary, secondary)
+     * @param <T> label resource type
+     */
+    <T> Pair<T, T> pickEach(Set<T> primary, Set<T> secondary) {
+        Set<T> intersection = Sets.intersection(primary, secondary);
+
+        if (!intersection.isEmpty()) {
+            // favor common
+            T picked = pickOne(intersection);
+            return Pair.of(picked, picked);
+        }
+
+        T pickedP = pickOne(primary);
+        T pickedS = pickOne(secondary);
+        return Pair.of(pickedP, pickedS);
+    }
+
+    /**
+     * Finds label resource, which can be used in common along the path.
+     *
+     * @param path path
+     * @param klass Label class
+     * @return Set of common resources
+     * @throws IntentCompilationException when there is no resource available
+     * @param <T> label resource type
+     */
+    <T> Set<T> commonLabelResource(Path path, Class<T> klass) {
+         Optional<Set<T>> common = path.links().stream()
+            .flatMap(link -> Stream.of(link.src(), link.dst()))
+            .distinct()
+            .map(cp -> getAvailableResourceValues(cp, klass))
+            .reduce(Sets::intersection);
+
+         if (!common.isPresent() || common.get().isEmpty()) {
+             throw new IntentCompilationException("No common label available for: " + path);
+         }
+         return common.get();
+    }
+
+    <T> Set<T> getAvailableResourceValues(ConnectPoint cp, Class<T> klass) {
+        return resourceService.getAvailableResourceValues(
+                                 resourceId(cp),
+                                 klass);
+    }
+
+}