[CORD-2838][CORD-2833] Revisit McastHandler and handle shortest paths with pair links

Includes also a refactoring of the path computation

Change-Id: Iff63780a3bb3e895e55c52211290c19d993e1905
diff --git a/apps/segmentrouting/BUCK b/apps/segmentrouting/BUCK
index f70eb88..c03ace3 100644
--- a/apps/segmentrouting/BUCK
+++ b/apps/segmentrouting/BUCK
@@ -9,5 +9,5 @@
     url = 'http://onosproject.org',
     included_bundles = BUNDLES,
     description = 'Segment routing application.',
-    required_apps = [ 'org.onosproject.route-service' ],
+    required_apps = [ 'org.onosproject.route-service', 'org.onosproject.mcast' ],
 )
diff --git a/apps/segmentrouting/app/BUCK b/apps/segmentrouting/app/BUCK
index 9674f76..41ae39c 100644
--- a/apps/segmentrouting/app/BUCK
+++ b/apps/segmentrouting/app/BUCK
@@ -8,6 +8,8 @@
     '//core/store/serializers:onos-core-serializers',
     '//incubator/api:onos-incubator-api',
     '//apps/route-service/api:onos-apps-route-service-api',
+    '//apps/mcast/api:onos-apps-mcast-api',
+    '//apps/mcast/cli:onos-apps-mcast-cli',
 ]
 
 TEST_DEPS = [
diff --git a/apps/segmentrouting/app/pom.xml b/apps/segmentrouting/app/pom.xml
index fbb4ecf..9d45cbb 100644
--- a/apps/segmentrouting/app/pom.xml
+++ b/apps/segmentrouting/app/pom.xml
@@ -51,14 +51,6 @@
             <artifactId>jersey-container-servlet</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.osgi</groupId>
-            <artifactId>org.osgi.compendium</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.osgi</groupId>
-            <artifactId>org.osgi.core</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.onosproject</groupId>
             <artifactId>onlab-junit</artifactId>
             <scope>test</scope>
@@ -88,5 +80,15 @@
             <classifier>tests</classifier>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-app-mcast-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-app-mcast-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
     </dependencies>
 </project>
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SRLinkWeigher.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SRLinkWeigher.java
new file mode 100644
index 0000000..01e07a7
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SRLinkWeigher.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.segmentrouting;
+
+import org.onlab.graph.DefaultEdgeWeigher;
+import org.onlab.graph.ScalarWeight;
+import org.onlab.graph.Weight;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.topology.LinkWeigher;
+import org.onosproject.net.topology.TopologyEdge;
+import org.onosproject.net.topology.TopologyVertex;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+
+import java.util.Set;
+
+/**
+ * Link weigher for multicast related path computations.
+ */
+public final class SRLinkWeigher
+        extends DefaultEdgeWeigher<TopologyVertex, TopologyEdge>
+        implements LinkWeigher {
+
+    private final SegmentRoutingManager srManager;
+    private final DeviceId srcPath;
+    private final Set<Link> linksToEnforce;
+
+    // Weight for the link to avoids. The high level idea is to build
+    // a constrained shortest path computation. 100 should provide a good
+    // threshold
+    public static final ScalarWeight LINK_TO_AVOID_WEIGHT = new ScalarWeight(HOP_WEIGHT_VALUE + 100);
+
+    /**
+     * Creates a SRLinkWeigher object.
+     *
+     * @param srManager SegmentRoutingManager object
+     * @param srcPath the source of the paths
+     * @param linksToEnforce links to be enforced by the path computation
+     */
+    public SRLinkWeigher(SegmentRoutingManager srManager, DeviceId srcPath,
+                         Set<Link> linksToEnforce) {
+        this.srManager = srManager;
+        this.srcPath = srcPath;
+        this.linksToEnforce = linksToEnforce;
+    }
+
+    @Override
+    public Weight weight(TopologyEdge edge) {
+        // 1) We need to avoid some particular paths like leaf-spine-leaf-*
+        // 2) Properly handle the pair links
+
+        // If the link is a pair link just return infinite value
+        if (isPairLink(edge.link())) {
+            return ScalarWeight.NON_VIABLE_WEIGHT;
+        }
+
+        // To avoid that the paths go through other leaves we need to influence
+        // the path computation to return infinite value for all other links having
+        // as a src a leaf different from the source we are passing to the weigher
+        DeviceId srcDeviceLink = edge.link().src().deviceId();
+        // Identify the link as leaf-spine link
+        boolean isLeafSpine;
+        try {
+            isLeafSpine = srManager.deviceConfiguration().isEdgeDevice(srcDeviceLink);
+        } catch (DeviceConfigNotFoundException e) {
+            isLeafSpine = false;
+        }
+        // If it is not the source just return infinite value
+        if (isLeafSpine && !srcDeviceLink.equals(srcPath)) {
+            return ScalarWeight.NON_VIABLE_WEIGHT;
+        }
+
+        // If the links are not in the list of the links to be enforce
+        if (!linksToEnforce.isEmpty() && !linksToEnforce.contains(edge.link())) {
+            // 100 should be a good confidence threshold
+            return LINK_TO_AVOID_WEIGHT;
+        }
+
+        // All other cases we return
+        return new ScalarWeight(HOP_WEIGHT_VALUE);
+    }
+
+    // Utility method to verify is a link is a pair-link
+    private boolean isPairLink(Link link) {
+        // Take src id, src port, dst id and dst port
+        final DeviceId srcId = link.src().deviceId();
+        final PortNumber srcPort = link.src().port();
+        final DeviceId dstId = link.dst().deviceId();
+        final PortNumber dstPort = link.dst().port();
+        // init as true
+        boolean isPairLink = true;
+        try {
+            // If one of this condition is not true; it is not a pair link
+            if (!(srManager.deviceConfiguration().isEdgeDevice(srcId) &&
+                    srManager.deviceConfiguration().isEdgeDevice(dstId) &&
+                    srManager.deviceConfiguration().getPairDeviceId(srcId).equals(dstId) &&
+                    srManager.deviceConfiguration().getPairLocalPort(srcId).equals(srcPort) &&
+                    srManager.deviceConfiguration().getPairLocalPort(dstId).equals(dstPort))) {
+                isPairLink = false;
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            // Configuration not provided
+            isPairLink = false;
+        }
+        return isPairLink;
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index b6aabf3..fe01fd3 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -44,6 +44,9 @@
 import org.onosproject.core.CoreService;
 import org.onosproject.event.Event;
 import org.onosproject.mastership.MastershipService;
+import org.onosproject.mcast.api.McastEvent;
+import org.onosproject.mcast.api.McastListener;
+import org.onosproject.mcast.api.MulticastRouteService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
@@ -77,9 +80,6 @@
 import org.onosproject.net.link.LinkEvent;
 import org.onosproject.net.link.LinkListener;
 import org.onosproject.net.link.LinkService;
-import org.onosproject.net.mcast.McastEvent;
-import org.onosproject.net.mcast.McastListener;
-import org.onosproject.net.mcast.MulticastRouteService;
 import org.onosproject.net.neighbour.NeighbourResolutionService;
 import org.onosproject.net.packet.InboundPacket;
 import org.onosproject.net.packet.PacketContext;
@@ -1096,10 +1096,11 @@
                         routeHandler.processRouteRemoved((RouteEvent) event);
                     } else if (event.type() == RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED) {
                         routeHandler.processAlternativeRoutesChanged((RouteEvent) event);
-                    } else if (event.type() == McastEvent.Type.SOURCE_ADDED ||
-                            event.type() == McastEvent.Type.SOURCE_UPDATED ||
-                            event.type() == McastEvent.Type.SINK_ADDED ||
-                            event.type() == McastEvent.Type.SINK_REMOVED ||
+                    } else if (event.type() == McastEvent.Type.SOURCES_ADDED ||
+                            event.type() == McastEvent.Type.SOURCES_REMOVED ||
+                            event.type() == McastEvent.Type.SINKS_ADDED ||
+                            event.type() == McastEvent.Type.SINKS_REMOVED ||
+                            event.type() == McastEvent.Type.ROUTE_ADDED ||
                             event.type() == McastEvent.Type.ROUTE_REMOVED) {
                         mcastHandler.processMcastEvent((McastEvent) event);
                     } else if (event.type() == NetworkConfigEvent.Type.CONFIG_ADDED) {
@@ -1503,10 +1504,10 @@
         @Override
         public void event(McastEvent event) {
             switch (event.type()) {
-                case SOURCE_ADDED:
-                case SOURCE_UPDATED:
-                case SINK_ADDED:
-                case SINK_REMOVED:
+                case SOURCES_ADDED:
+                case SOURCES_REMOVED:
+                case SINKS_ADDED:
+                case SINKS_REMOVED:
                 case ROUTE_REMOVED:
                     log.trace("Schedule Mcast event {}", event);
                     scheduleEventHandlerIfNotScheduled(event);
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
index 341c87d..a713130 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
@@ -13,13 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.onosproject.segmentrouting.cli;
 
 import com.google.common.collect.Maps;
-import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
 import org.onlab.packet.IpAddress;
 import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.mcast.cli.McastGroupCompleter;
 import org.onosproject.net.DeviceId;
 import org.onosproject.segmentrouting.SegmentRoutingService;
 import org.onosproject.segmentrouting.storekey.McastStoreKey;
@@ -37,19 +39,24 @@
         description = "Lists all mcast nextids")
 public class McastNextListCommand extends AbstractShellCommand {
 
+    // OSGi workaround to introduce package dependency
+    McastGroupCompleter completer;
+
     // Format for group line
     private static final String FORMAT_MAPPING = "group=%s, deviceIds-nextIds=%s";
 
-    @Argument(index = 0, name = "mcastIp", description = "mcast Ip",
+    @Option(name = "-gAddr", aliases = "--groupAddress",
+            description = "IP Address of the multicast group",
+            valueToShowInHelp = "224.0.0.0",
             required = false, multiValued = false)
-    String mcastIp;
+    String gAddr = null;
 
     @Override
     protected void execute() {
         // Verify mcast group
         IpAddress mcastGroup = null;
-        if (!isNullOrEmpty(mcastIp)) {
-            mcastGroup = IpAddress.valueOf(mcastIp);
+        if (!isNullOrEmpty(gAddr)) {
+            mcastGroup = IpAddress.valueOf(gAddr);
 
         }
         // Get SR service
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
index dc86bef..d1de73c 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
@@ -18,10 +18,11 @@
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
-import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
 import org.onlab.packet.IpAddress;
 import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.mcast.cli.McastGroupCompleter;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
 import org.onosproject.segmentrouting.SegmentRoutingService;
@@ -46,21 +47,26 @@
         description = "Lists all mcast trees")
 public class McastTreeListCommand extends AbstractShellCommand {
 
+    // OSGi workaround to introduce package dependency
+    McastGroupCompleter completer;
+
     // Format for group line
     private static final String G_FORMAT_MAPPING = "group=%s, ingress=%s, transit=%s, egress=%s";
     // Format for sink line
     private static final String S_FORMAT_MAPPING = "\tsink=%s\tpath=%s";
 
-    @Argument(index = 0, name = "mcastIp", description = "mcast Ip",
+    @Option(name = "-gAddr", aliases = "--groupAddress",
+            description = "IP Address of the multicast group",
+            valueToShowInHelp = "224.0.0.0",
             required = false, multiValued = false)
-    String mcastIp;
+    String gAddr = null;
 
     @Override
     protected void execute() {
         // Verify mcast group
         IpAddress mcastGroup = null;
-        if (!isNullOrEmpty(mcastIp)) {
-            mcastGroup = IpAddress.valueOf(mcastIp);
+        if (!isNullOrEmpty(gAddr)) {
+            mcastGroup = IpAddress.valueOf(gAddr);
 
         }
         // Get SR service
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java
index a791ecb..968ad3b 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java
@@ -33,6 +33,9 @@
 import org.onosproject.cluster.NodeId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
+import org.onosproject.mcast.api.McastEvent;
+import org.onosproject.mcast.api.McastRoute;
+import org.onosproject.mcast.api.McastRouteUpdate;
 import org.onosproject.net.config.basics.McastConfig;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
@@ -54,11 +57,10 @@
 import org.onosproject.net.flowobjective.ForwardingObjective;
 import org.onosproject.net.flowobjective.NextObjective;
 import org.onosproject.net.flowobjective.ObjectiveContext;
-import org.onosproject.net.mcast.McastEvent;
-import org.onosproject.net.mcast.McastRoute;
-import org.onosproject.net.mcast.McastRouteInfo;
+import org.onosproject.net.topology.LinkWeigher;
 import org.onosproject.net.topology.Topology;
 import org.onosproject.net.topology.TopologyService;
+import org.onosproject.segmentrouting.SRLinkWeigher;
 import org.onosproject.segmentrouting.SegmentRoutingManager;
 import org.onosproject.segmentrouting.SegmentRoutingService;
 import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
@@ -78,6 +80,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
@@ -88,10 +91,9 @@
 
 import static java.util.concurrent.Executors.newScheduledThreadPool;
 import static org.onlab.util.Tools.groupedThreads;
+
+import static org.onosproject.mcast.api.McastEvent.Type.*;
 import static org.onosproject.net.flow.criteria.Criterion.Type.VLAN_VID;
-import static org.onosproject.net.mcast.McastEvent.Type.ROUTE_REMOVED;
-import static org.onosproject.net.mcast.McastEvent.Type.SOURCE_ADDED;
-import static org.onosproject.net.mcast.McastEvent.Type.SOURCE_UPDATED;
 import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
 import static org.onosproject.segmentrouting.mcast.McastRole.EGRESS;
 import static org.onosproject.segmentrouting.mcast.McastRole.INGRESS;
@@ -139,63 +141,116 @@
 
     private void enqueueMcastEvent(McastEvent mcastEvent) {
         log.debug("Enqueue mcastEvent {}", mcastEvent);
-        final McastRouteInfo mcastRouteInfo = mcastEvent.subject();
+        final McastRouteUpdate mcastRouteUpdate = mcastEvent.subject();
+        final McastRouteUpdate prevUpdate = mcastEvent.prevSubject();
+        final IpAddress group = prevUpdate.route().group();
         // Let's create the keys of the cache
         ImmutableSet.Builder<ConnectPoint> sinksBuilder = ImmutableSet.builder();
+        Set<ConnectPoint> sinks;
         // For this event we will have a set of sinks
-        if (mcastEvent.type() == SOURCE_ADDED ||
-                mcastEvent.type() == SOURCE_UPDATED ||
-                mcastEvent.type() == ROUTE_REMOVED) {
-            // Add all the sinks
-            sinksBuilder.addAll(mcastRouteInfo.sinks());
+        if (mcastEvent.type() == SOURCES_ADDED ||
+                mcastEvent.type() == SOURCES_REMOVED) {
+            // FIXME To be addressed with multiple sources support
+            sinks = mcastRouteUpdate.sinks()
+                    .values()
+                    .stream()
+                    .flatMap(Collection::stream)
+                    .collect(Collectors.toSet());
         } else {
-            // We have just one sink in this case
-            ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
-            // It is always true, unless something of bad happened
-            // in the mcast route store
-            if (sink != null) {
-                sinksBuilder.add(sink);
+            Set<ConnectPoint> prevSinks = prevUpdate.sinks()
+                    .values()
+                    .stream()
+                    .flatMap(Collection::stream)
+                    .collect(Collectors.toSet());
+            if (mcastEvent.type() == ROUTE_REMOVED) {
+                // Get the old sinks, since current subject is null
+                sinks = prevSinks;
+            } else {
+                // Get new sinks
+                Set<ConnectPoint> newsinks = mcastRouteUpdate.sinks()
+                        .values()
+                        .stream()
+                        .flatMap(Collection::stream)
+                        .collect(Collectors.toSet());
+                // If it is a SINKS_ADDED event
+                if (mcastEvent.type() == SINKS_ADDED) {
+                    // Let's do the difference between current and prev subjects
+                    sinks = Sets.difference(newsinks, prevSinks);
+                } else {
+                    // Let's do the difference between prev and current subjects
+                    sinks = Sets.difference(prevSinks, newsinks);
+                }
             }
         }
+        // Add all the sinks
+        sinksBuilder.addAll(sinks);
         // Push the elements in the cache
         sinksBuilder.build().forEach(sink -> {
-            McastCacheKey cacheKey = new McastCacheKey(mcastRouteInfo.route().group(),
-                                                       sink);
+            McastCacheKey cacheKey = new McastCacheKey(group, sink);
             mcastEventCache.put(cacheKey, mcastEvent);
         });
     }
 
     private void dequeueMcastEvent(McastEvent mcastEvent) {
         log.debug("Dequeue mcastEvent {}", mcastEvent);
-        final McastRouteInfo mcastRouteInfo = mcastEvent.subject();
+        final McastRouteUpdate mcastRouteUpdate = mcastEvent.subject();
+        final McastRouteUpdate prevUpdate = mcastEvent.prevSubject();
         // Get source, mcast group
-        ConnectPoint source = mcastRouteInfo.source().orElse(null);
-        IpAddress mcastIp = mcastRouteInfo.route().group();
+        // FIXME To be addressed with multiple sources support
+        ConnectPoint prevSource = prevUpdate.sources()
+                .stream()
+                .findFirst()
+                .orElse(null);
+        IpAddress mcastIp = prevUpdate.route().group();
+        Set<ConnectPoint> prevSinks = prevUpdate.sinks()
+                .values()
+                .stream()
+                .flatMap(Collection::stream)
+                .collect(Collectors.toSet());
+        Set<ConnectPoint> newSinks;
+        // Sinks to handled by SINKS_ADDED and SINKS_REMOVED procedures
+        Set<ConnectPoint> sinks;
         // According to the event type let's call the proper method
         switch (mcastEvent.type()) {
-            case SOURCE_ADDED:
-                // Get all the sinks and process
-                Set<ConnectPoint> sinks = mcastRouteInfo.sinks();
-                sinks.forEach(sink -> processSinkAddedInternal(source, sink, mcastIp));
+            case SOURCES_ADDED:
+                // FIXME To be addressed with multiple sources support
+                // Get all the sinks
+                //Set<ConnectPoint> sinks = mcastRouteInfo.sinks();
+                // Compute the Mcast tree
+                //Map<ConnectPoint, List<Path>> mcasTree = computeSinkMcastTree(source.deviceId(), sinks);
+                // Process the given sinks using the pre-computed paths
+                //mcasTree.forEach((sink, paths) -> processSinkAddedInternal(source, sink, mcastIp, paths));
                 break;
-            case SOURCE_UPDATED:
+            case SOURCES_REMOVED:
+                // FIXME To be addressed with multiple sources support
                 // Get old source
-                ConnectPoint oldSource = mcastEvent.prevSubject().source().orElse(null);
+                //ConnectPoint oldSource = mcastEvent.prevSubject().source().orElse(null);
                 // Just the first cached element will be processed
-                processSourceUpdatedInternal(mcastIp, source, oldSource);
+                //processSourceUpdatedInternal(mcastIp, source, oldSource);
                 break;
             case ROUTE_REMOVED:
                 // Process the route removed, just the first cached element will be processed
-                processRouteRemovedInternal(source, mcastIp);
+                processRouteRemovedInternal(prevSource, mcastIp);
                 break;
-            case SINK_ADDED:
-                // Get the only sink and process
-                ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
-                processSinkAddedInternal(source, sink, mcastIp);
+            case SINKS_ADDED:
+                // Get the only sinks to be processed (new ones)
+                newSinks = mcastRouteUpdate.sinks()
+                        .values()
+                        .stream()
+                        .flatMap(Collection::stream)
+                        .collect(Collectors.toSet());
+                sinks = Sets.difference(newSinks, prevSinks);
+                sinks.forEach(sink -> processSinkAddedInternal(prevSource, sink, mcastIp, null));
                 break;
-            case SINK_REMOVED:
-                sink = mcastRouteInfo.sink().orElse(null);
-                processSinkRemovedInternal(source, sink, mcastIp);
+            case SINKS_REMOVED:
+                // Get the only sinks to be processed (old ones)
+                newSinks = mcastRouteUpdate.sinks()
+                        .values()
+                        .stream()
+                        .flatMap(Collection::stream)
+                        .collect(Collectors.toSet());
+                sinks = Sets.difference(prevSinks, newSinks);
+                sinks.forEach(sink -> processSinkRemovedInternal(prevSource, sink, mcastIp));
                 break;
             default:
                 break;
@@ -283,11 +338,26 @@
      */
     public void init() {
         srManager.multicastRouteService.getRoutes().forEach(mcastRoute -> {
-            ConnectPoint source = srManager.multicastRouteService.fetchSource(mcastRoute);
-            Set<ConnectPoint> sinks = srManager.multicastRouteService.fetchSinks(mcastRoute);
-            sinks.forEach(sink -> {
-                processSinkAddedInternal(source, sink, mcastRoute.group());
-            });
+            // FIXME To be addressed with multiple sources support
+            ConnectPoint source = srManager.multicastRouteService.sources(mcastRoute)
+                    .stream()
+                    .findFirst()
+                    .orElse(null);
+            Set<ConnectPoint> sinks = srManager.multicastRouteService.sinks(mcastRoute);
+            // Filter out all the working sinks, we do not want to move them
+            sinks = sinks.stream()
+                    .filter(sink -> {
+                        McastStoreKey mcastKey = new McastStoreKey(mcastRoute.group(), sink.deviceId());
+                        Versioned<NextObjective> verMcastNext = mcastNextObjStore.get(mcastKey);
+                        return verMcastNext == null ||
+                                !getPorts(verMcastNext.value().next()).contains(sink.port());
+                    })
+                    .collect(Collectors.toSet());
+            // Compute the Mcast tree
+            Map<ConnectPoint, List<Path>> mcasTree = computeSinkMcastTree(source.deviceId(), sinks);
+            // Process the given sinks using the pre-computed paths
+            mcasTree.forEach((sink, paths) -> processSinkAddedInternal(source, sink,
+                                                                       mcastRoute.group(), paths));
         });
     }
 
@@ -306,12 +376,6 @@
      */
     public void processMcastEvent(McastEvent event) {
         log.info("process {}", event);
-        // Verify if it is a complete event
-        McastRouteInfo mcastRouteInfo = event.subject();
-        if (!mcastRouteInfo.isComplete()) {
-            log.info("Incompleted McastRouteInfo. Abort {}", event.type());
-            return;
-        }
         // Just enqueue for now
         enqueueMcastEvent(event);
     }
@@ -451,7 +515,8 @@
             }
 
             // If this is the last sink on the device, also update upstream
-            Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+            Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(),
+                                               mcastIp, null);
             if (mcastPath.isPresent()) {
                 List<Link> links = Lists.newArrayList(mcastPath.get().links());
                 Collections.reverse(links);
@@ -482,7 +547,7 @@
      * @param mcastIp multicast group IP address
      */
     private void processSinkAddedInternal(ConnectPoint source, ConnectPoint sink,
-            IpAddress mcastIp) {
+            IpAddress mcastIp, List<Path> allPaths) {
         lastMcastChange = Instant.now();
         mcastLock();
         try {
@@ -511,7 +576,8 @@
             }
 
             // Find a path. If present, create/update groups and flows for each hop
-            Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+            Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(),
+                                               mcastIp, allPaths);
             if (mcastPath.isPresent()) {
                 List<Link> links = mcastPath.get().links();
 
@@ -581,7 +647,7 @@
                 // Continue only when this instance is the master of source device
                 if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
                     log.debug("Skip {} due to lack of mastership of the source device {}",
-                             source.deviceId());
+                             mcastIp, source.deviceId());
                     return;
                 }
 
@@ -592,9 +658,15 @@
                 // Remove transit-facing ports on the ingress device
                 removeIngressTransitPorts(mcastIp, ingressDevice, source);
 
+                // Compute mcast tree for the the egress devices
+                Map<DeviceId, List<Path>> mcastTree = computeMcastTree(ingressDevice, egressDevices);
+
                 // Construct a new path for each egress device
-                egressDevices.forEach(egressDevice -> {
-                    Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+                mcastTree.forEach((egressDevice, paths) -> {
+                    // We try to enforce the sinks path on the mcast tree
+                    Optional<Path> mcastPath = getPath(ingressDevice, egressDevice,
+                                                       mcastIp, paths);
+                    // If a path is present, let's install it
                     if (mcastPath.isPresent()) {
                         installPath(mcastIp, source, mcastPath.get());
                     } else {
@@ -680,9 +752,14 @@
                             return;
                         }
                     }
+
+                    // Compute mcast tree for the the egress devices
+                    Map<DeviceId, List<Path>> mcastTree = computeMcastTree(ingressDevice, egressDevices);
+
                     // Construct a new path for each egress device
-                    egressDevices.forEach(egressDevice -> {
-                        Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+                    mcastTree.forEach((egressDevice, paths) -> {
+                        Optional<Path> mcastPath = getPath(ingressDevice, egressDevice,
+                                                           mcastIp, null);
                         // If there is a new path
                         if (mcastPath.isPresent()) {
                             // Let's install the new mcast path for this egress
@@ -1072,31 +1149,157 @@
         return builder.build();
     }
 
-    // Utility method to verify is a link is a pair-link
-    private boolean isPairLink(Link link) {
-        // Take src id, src port, dst id and dst port
-        final DeviceId srcId = link.src().deviceId();
-        final PortNumber srcPort = link.src().port();
-        final DeviceId dstId = link.dst().deviceId();
-        final PortNumber dstPort = link.dst().port();
-        // init as true
-        boolean isPairLink = true;
-        try {
-            // If one of this condition is not true; it is not a pair link
-            if (!(srManager.deviceConfiguration().isEdgeDevice(srcId) &&
-                  srManager.deviceConfiguration().isEdgeDevice(dstId) &&
-                  srManager.deviceConfiguration().getPairDeviceId(srcId).equals(dstId) &&
-                  srManager.deviceConfiguration().getPairLocalPort(srcId).equals(srcPort) &&
-                  srManager.deviceConfiguration().getPairLocalPort(dstId).equals(dstPort))) {
-                    isPairLink = false;
-                }
-        } catch (DeviceConfigNotFoundException e) {
-            // Configuration not provided
-            log.warn("Could not check if the link {} is pairlink "
-                             + "config not yet provided", link);
-            isPairLink = false;
+    /**
+     * Go through all the paths, looking for shared links to be used
+     * in the final path computation.
+     *
+     * @param egresses egress devices
+     * @param availablePaths all the available paths towards the egress
+     * @return shared links between egress devices
+     */
+    private Set<Link> exploreMcastTree(Set<DeviceId> egresses,
+                                       Map<DeviceId, List<Path>> availablePaths) {
+        // Length of the shortest path
+        int minLength = Integer.MAX_VALUE;
+        int length;
+        // Current paths
+        List<Path> currentPaths;
+        // Verify the source can still reach all the egresses
+        for (DeviceId egress : egresses) {
+            // From the source we cannot reach all the sinks
+            // just continue and let's figured out after
+            currentPaths = availablePaths.get(egress);
+            if (currentPaths.isEmpty()) {
+                continue;
+            }
+            // Get the length of the first one available,
+            // update the min length and save the paths
+            length = currentPaths.get(0).links().size();
+            if (length < minLength) {
+                minLength = length;
+            }
         }
-        return isPairLink;
+        // If there are no paths
+        if (minLength == Integer.MAX_VALUE) {
+            return Collections.emptySet();
+        }
+        // Iterate looking for shared links
+        int index = 0;
+        // Define the sets for the intersection
+        Set<Link> sharedLinks = Sets.newHashSet();
+        Set<Link> currentSharedLinks;
+        Set<Link> currentLinks;
+        DeviceId deviceToRemove = null;
+        // Let's find out the shared links
+        while (index < minLength) {
+            // Initialize the intersection with the paths related to the first egress
+            currentPaths = availablePaths.get(
+                    egresses.stream()
+                            .findFirst()
+                            .orElse(null)
+            );
+            currentSharedLinks = Sets.newHashSet();
+            // Iterate over the paths and take the "index" links
+            for (Path path : currentPaths) {
+                currentSharedLinks.add(path.links().get(index));
+            }
+            // Iterate over the remaining egress
+            for (DeviceId egress : egresses) {
+                // Iterate over the paths and take the "index" links
+                currentLinks = Sets.newHashSet();
+                for (Path path : availablePaths.get(egress)) {
+                    currentLinks.add(path.links().get(index));
+                }
+                // Do intersection
+                currentSharedLinks = Sets.intersection(currentSharedLinks, currentLinks);
+                // If there are no shared paths exit and record the device to remove
+                // we have to retry with a subset of sinks
+                if (currentSharedLinks.isEmpty()) {
+                    deviceToRemove = egress;
+                    index = minLength;
+                    break;
+                }
+            }
+            sharedLinks.addAll(currentSharedLinks);
+            index++;
+        }
+        // If the shared links is empty and there are egress
+        // let's retry another time with less sinks, we can
+        // still build optimal subtrees
+        if (sharedLinks.isEmpty() && egresses.size() > 1 && deviceToRemove != null) {
+            egresses.remove(deviceToRemove);
+            sharedLinks = exploreMcastTree(egresses, availablePaths);
+        }
+        return sharedLinks;
+    }
+
+    /**
+     * Build Mcast tree having as root the given source and as leaves the given egress points.
+     *
+     * @param source source of the tree
+     * @param sinks leaves of the tree
+     * @return the computed Mcast tree
+     */
+    private Map<ConnectPoint, List<Path>> computeSinkMcastTree(DeviceId source,
+                                                                Set<ConnectPoint> sinks) {
+        // Get the egress devices, remove source from the egress if present
+        Set<DeviceId> egresses = sinks.stream()
+                .map(ConnectPoint::deviceId)
+                .filter(deviceId -> !deviceId.equals(source))
+                .collect(Collectors.toSet());
+        Map<DeviceId, List<Path>> mcastTree = computeMcastTree(source, egresses);
+        // Build final tree nad return it as it is
+        final Map<ConnectPoint, List<Path>> finalTree = Maps.newHashMap();
+        mcastTree.forEach((egress, paths) ->
+            sinks.stream().filter(sink -> sink.deviceId().equals(egress))
+                    .forEach(sink -> finalTree.put(sink, mcastTree.get(sink.deviceId()))));
+        return finalTree;
+    }
+
+    /**
+     * Build Mcast tree having as root the given source and as leaves the given egress.
+     *
+     * @param source source of the tree
+     * @param egresses leaves of the tree
+     * @return the computed Mcast tree
+     */
+    private Map<DeviceId, List<Path>> computeMcastTree(DeviceId source,
+                                                       Set<DeviceId> egresses) {
+        // Pre-compute all the paths
+        Map<DeviceId, List<Path>> availablePaths = Maps.newHashMap();
+        // No links to enforce
+        egresses.forEach(egress -> availablePaths.put(egress, getPaths(source, egress,
+                                                                       Collections.emptySet())));
+        // Explore the topology looking for shared links amongst the egresses
+        Set<Link> linksToEnforce = exploreMcastTree(Sets.newHashSet(egresses), availablePaths);
+        // Remove all the paths from the previous computation
+        availablePaths.clear();
+        // Build the final paths enforcing the shared links between egress devices
+        egresses.forEach(egress -> availablePaths.put(egress, getPaths(source, egress,
+                                                                       linksToEnforce)));
+        return availablePaths;
+    }
+
+    /**
+     * Gets path from src to dst computed using the custom link weigher.
+     *
+     * @param src source device ID
+     * @param dst destination device ID
+     * @return list of paths from src to dst
+     */
+    private List<Path> getPaths(DeviceId src, DeviceId dst, Set<Link> linksToEnforce) {
+        // Takes a snapshot of the topology
+        final Topology currentTopology = topologyService.currentTopology();
+        // Build a specific link weigher for this path computation
+        final LinkWeigher linkWeigher = new SRLinkWeigher(srManager, src, linksToEnforce);
+        // We will use our custom link weigher for our path
+        // computations and build the list of valid paths
+        List<Path> allPaths = Lists.newArrayList(
+                topologyService.getPaths(currentTopology, src, dst, linkWeigher)
+        );
+        // If there are no valid paths, just exit
+        log.debug("{} path(s) found from {} to {}", allPaths.size(), src, dst);
+        return allPaths;
     }
 
     /**
@@ -1107,18 +1310,17 @@
      * @param src source device ID
      * @param dst destination device ID
      * @param mcastIp multicast group
+     * @param allPaths paths list
      * @return an optional path from src to dst
      */
-    private Optional<Path> getPath(DeviceId src, DeviceId dst, IpAddress mcastIp) {
-        // Takes a snapshot of the topology
-        final Topology currentTopology = topologyService.currentTopology();
-        List<Path> allPaths = Lists.newArrayList(
-                topologyService.getPaths(currentTopology, src, dst)
-        );
-        // Create list of valid paths
-        allPaths.removeIf(path -> path.links().stream().anyMatch(this::isPairLink));
-        // If there are no valid paths, just exit
-        log.debug("{} path(s) found from {} to {}", allPaths.size(), src, dst);
+    private Optional<Path> getPath(DeviceId src, DeviceId dst,
+                                   IpAddress mcastIp, List<Path> allPaths) {
+        // Firstly we get all the valid paths, if the supplied are null
+        if (allPaths == null) {
+            allPaths = getPaths(src, dst, Collections.emptySet());
+        }
+
+        // If there are no paths just exit
         if (allPaths.isEmpty()) {
             return Optional.empty();
         }
@@ -1205,7 +1407,7 @@
         return mcastRoleStore.entrySet().stream()
                 .filter(entry -> entry.getKey().mcastIp().equals(mcastIp) &&
                         entry.getValue().value() == role)
-                .map(Map.Entry::getKey).map(McastStoreKey::deviceId)
+                .map(Entry::getKey).map(McastStoreKey::deviceId)
                 .collect(Collectors.toSet());
     }
 
@@ -1215,11 +1417,15 @@
      * @param mcastIp multicast IP
      * @return source connect point or null if not found
      */
+    // FIXME To be addressed with multiple sources support
     private ConnectPoint getSource(IpAddress mcastIp) {
-        return srManager.multicastRouteService.getRoutes().stream()
-                .filter(mcastRoute -> mcastRoute.group().equals(mcastIp))
-                .map(mcastRoute -> srManager.multicastRouteService.fetchSource(mcastRoute))
-                .findAny().orElse(null);
+        // FIXME we should support different types of routes
+        McastRoute mcastRoute = srManager.multicastRouteService.getRoutes().stream()
+                .filter(mcastRouteInternal -> mcastRouteInternal.group().equals(mcastIp))
+                .findFirst().orElse(null);
+        return mcastRoute == null ? null : srManager.multicastRouteService.sources(mcastRoute)
+                .stream()
+                .findFirst().orElse(null);
     }
     /**
      * Gets sinks of given multicast group.
@@ -1228,10 +1434,12 @@
      * @return set of sinks or empty set if not found
      */
     private Set<ConnectPoint> getSinks(IpAddress mcastIp) {
-        return srManager.multicastRouteService.getRoutes().stream()
-                .filter(mcastRoute -> mcastRoute.group().equals(mcastIp))
-                .map(mcastRoute -> srManager.multicastRouteService.fetchSinks(mcastRoute))
-                .findAny().orElse(Collections.emptySet());
+        // FIXME we should support different types of routes
+        McastRoute mcastRoute = srManager.multicastRouteService.getRoutes().stream()
+                .filter(mcastRouteInternal -> mcastRouteInternal.group().equals(mcastIp))
+                .findFirst().orElse(null);
+        return mcastRoute == null ?
+                Collections.emptySet() : srManager.multicastRouteService.sinks(mcastRoute);
     }
 
     /**
@@ -1246,7 +1454,7 @@
         return mcastNextObjStore.entrySet().stream()
                 .filter(entry -> entry.getKey().deviceId().equals(deviceId) &&
                         getPorts(entry.getValue().value().next()).contains(port))
-                .map(Map.Entry::getKey).map(McastStoreKey::mcastIp)
+                .map(Entry::getKey).map(McastStoreKey::mcastIp)
                 .collect(Collectors.toSet());
     }
 
@@ -1259,7 +1467,7 @@
     private Set<IpAddress> getAffectedGroups(DeviceId deviceId) {
         return mcastNextObjStore.entrySet().stream()
                 .filter(entry -> entry.getKey().deviceId().equals(deviceId))
-                .map(Map.Entry::getKey).map(McastStoreKey::mcastIp)
+                .map(Entry::getKey).map(McastStoreKey::mcastIp)
                 .collect(Collectors.toSet());
     }
 
@@ -1433,7 +1641,10 @@
             // Iterates over the route and updates properly the filtering objective
             // on the source device.
             srManager.multicastRouteService.getRoutes().forEach(mcastRoute -> {
-                ConnectPoint source = srManager.multicastRouteService.fetchSource(mcastRoute);
+                // FIXME To be addressed with multiple sources support
+                ConnectPoint source = srManager.multicastRouteService.sources(mcastRoute)
+                        .stream()
+                        .findFirst().orElse(null);
                 if (source.deviceId().equals(deviceId) && source.port().equals(portNum)) {
                     if (install) {
                         addFilterToDevice(deviceId, portNum, vlanId, mcastRoute.group(), INGRESS);
@@ -1570,12 +1781,12 @@
         if (mcastIp != null) {
             return mcastNextObjStore.entrySet().stream()
                     .filter(mcastEntry -> mcastIp.equals(mcastEntry.getKey().mcastIp()))
-                    .collect(Collectors.toMap(Map.Entry::getKey,
+                    .collect(Collectors.toMap(Entry::getKey,
                                               entry -> entry.getValue().value().id()));
         }
         // Otherwise take all the groups
         return mcastNextObjStore.entrySet().stream()
-                .collect(Collectors.toMap(Map.Entry::getKey,
+                .collect(Collectors.toMap(Entry::getKey,
                                           entry -> entry.getValue().value().id()));
     }
 
@@ -1584,12 +1795,12 @@
         if (mcastIp != null) {
             return mcastRoleStore.entrySet().stream()
                     .filter(mcastEntry -> mcastIp.equals(mcastEntry.getKey().mcastIp()))
-                    .collect(Collectors.toMap(Map.Entry::getKey,
+                    .collect(Collectors.toMap(Entry::getKey,
                                               entry -> entry.getValue().value()));
         }
         // Otherwise take all the groups
         return mcastRoleStore.entrySet().stream()
-                .collect(Collectors.toMap(Map.Entry::getKey,
+                .collect(Collectors.toMap(Entry::getKey,
                                           entry -> entry.getValue().value()));
     }
 
diff --git a/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index cfd7502..647eb49 100644
--- a/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -71,24 +71,22 @@
         </command>
         <command>
             <action class="org.onosproject.segmentrouting.cli.McastNextListCommand"/>
-            <completers>
-                <ref component-id="mcastGroupCompleter"/>
-                <ref component-id="nullCompleter"/>
-            </completers>
+            <optional-completers>
+                <entry key="-gAddr" value-ref="mcastGroupCompleter"/>
+            </optional-completers>
         </command>
         <command>
             <action class="org.onosproject.segmentrouting.cli.McastTreeListCommand"/>
-            <completers>
-                <ref component-id="mcastGroupCompleter"/>
-                <ref component-id="nullCompleter"/>
-            </completers>
+            <optional-completers>
+                <entry key="-gAddr" value-ref="mcastGroupCompleter"/>
+            </optional-completers>
         </command>
     </command-bundle>
 
     <bean id="nullCompleter" class="org.apache.karaf.shell.console.completer.NullCompleter"/>
     <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/>
     <bean id="pseudowireIdCompleter" class="org.onosproject.segmentrouting.cli.PseudowireIdCompleter"/>
-    <bean id="mcastGroupCompleter" class="org.onosproject.cli.net.McastGroupCompleter"/>
+    <bean id="mcastGroupCompleter" class="org.onosproject.mcast.cli.McastGroupCompleter"/>
 
 </blueprint>