CORD-348 Fabric multicast support - error handling

Automatically failover to backup spine if
- ingress - transit link down
- transit - egress link down
- transit device down

Can recover from fatal error with human involved
- ingress switch down
- egress switch down
- all links to spine down

Scan through McastRouteStore when
- SR activate
- link up

Also include following features
- Use flow objective context in McastHandler
- Update Mcast VLAN config sample

Change-Id: I75007d9efd7646e7c4e57fa6d3fc6943543153cf
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastHandler.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastHandler.java
index 74db457..8139e27 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastHandler.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastHandler.java
@@ -42,25 +42,33 @@
 import org.onosproject.net.flowobjective.DefaultFilteringObjective;
 import org.onosproject.net.flowobjective.DefaultForwardingObjective;
 import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
 import org.onosproject.net.flowobjective.FilteringObjective;
 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.McastRouteInfo;
 import org.onosproject.net.topology.TopologyService;
-import org.onosproject.segmentrouting.storekey.McastNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.ConsistentMap;
 import org.onosproject.store.service.Serializer;
 import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkState;
 
 /**
  * Handles multicast-related events.
@@ -71,8 +79,27 @@
     private final ApplicationId coreAppId;
     private StorageService storageService;
     private TopologyService topologyService;
-    private final KryoNamespace.Builder kryoBuilder;
-    private final ConsistentMap<McastNextObjectiveStoreKey, NextObjective> mcastNextObjStore;
+    private final ConsistentMap<McastStoreKey, NextObjective> mcastNextObjStore;
+    private final KryoNamespace.Builder mcastKryo;
+    private final ConsistentMap<McastStoreKey, McastRole> mcastRoleStore;
+
+    /**
+     * Role in the multicast tree.
+     */
+    public enum McastRole {
+        /**
+         * The device is the ingress device of this group.
+         */
+        INGRESS,
+        /**
+         * The device is the transit device of this group.
+         */
+        TRANSIT,
+        /**
+         * The device is the egress device of this group.
+         */
+        EGRESS
+    }
 
     /**
      * Constructs the McastEventHandler.
@@ -81,19 +108,36 @@
      */
     public McastHandler(SegmentRoutingManager srManager) {
         coreAppId = srManager.coreService.getAppId(CoreService.CORE_APP_NAME);
-
         this.srManager = srManager;
         this.storageService = srManager.storageService;
         this.topologyService = srManager.topologyService;
-
-        kryoBuilder = new KryoNamespace.Builder()
+        mcastKryo = new KryoNamespace.Builder()
                 .register(KryoNamespaces.API)
-                .register(McastNextObjectiveStoreKey.class);
+                .register(McastStoreKey.class)
+                .register(McastRole.class);
         mcastNextObjStore = storageService
-                .<McastNextObjectiveStoreKey, NextObjective>consistentMapBuilder()
+                .<McastStoreKey, NextObjective>consistentMapBuilder()
                 .withName("onos-mcast-nextobj-store")
-                .withSerializer(Serializer.using(kryoBuilder.build()))
+                .withSerializer(Serializer.using(mcastKryo.build()))
                 .build();
+        mcastRoleStore = storageService
+                .<McastStoreKey, McastRole>consistentMapBuilder()
+                .withName("onos-mcast-role-store")
+                .withSerializer(Serializer.using(mcastKryo.build()))
+                .build();
+    }
+
+    /**
+     * Read initial multicast from mcast store.
+     */
+    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());
+            });
+        });
     }
 
     /**
@@ -166,6 +210,9 @@
 
         // Process the egress device
         boolean isLast = removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan);
+        if (isLast) {
+            mcastRoleStore.remove(new McastStoreKey(mcastIp, sink.deviceId()));
+        }
 
         // If this is the last sink on the device, also update upstream
         Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
@@ -176,6 +223,7 @@
                 if (isLast) {
                     isLast = removePortFromDevice(link.src().deviceId(), link.src().port(),
                             mcastIp, assignedVlan);
+                    mcastRoleStore.remove(new McastStoreKey(mcastIp, link.src().deviceId()));
                 }
             }
         }
@@ -192,6 +240,9 @@
             IpAddress mcastIp) {
         VlanId assignedVlan = assignedVlan();
 
+        // Process the ingress device
+        addFilterToDevice(source.deviceId(), source.port(), assignedVlan);
+
         // When source and sink are on the same device
         if (source.deviceId().equals(sink.deviceId())) {
             // Source and sink are on even the same port. There must be something wrong.
@@ -200,25 +251,91 @@
                 return;
             }
             addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan);
+            mcastRoleStore.put(new McastStoreKey(mcastIp, sink.deviceId()), McastRole.INGRESS);
             return;
         }
 
-        // Process the ingress device
-        addFilterToDevice(source.deviceId(), source.port(), assignedVlan);
-
         // Find a path. If present, create/update groups and flows for each hop
         Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
         if (mcastPath.isPresent()) {
-            mcastPath.get().links().forEach(link -> {
+            List<Link> links = mcastPath.get().links();
+            checkState(links.size() == 2,
+                    "Path in leaf-spine topology should always be two hops: ", links);
+
+            links.forEach(link -> {
                 addFilterToDevice(link.dst().deviceId(), link.dst().port(), assignedVlan);
                 addPortToDevice(link.src().deviceId(), link.src().port(), mcastIp, assignedVlan);
             });
+
             // Process the egress device
             addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan);
+
+            // Setup mcast roles
+            mcastRoleStore.put(new McastStoreKey(mcastIp, source.deviceId()),
+                    McastRole.INGRESS);
+            mcastRoleStore.put(new McastStoreKey(mcastIp, links.get(0).dst().deviceId()),
+                    McastRole.TRANSIT);
+            mcastRoleStore.put(new McastStoreKey(mcastIp, sink.deviceId()),
+                    McastRole.EGRESS);
+        } else {
+            log.warn("Unable to find a path from {} to {}. Abort sinkAdded",
+                    source.deviceId(), sink.deviceId());
         }
     }
 
     /**
+     * Processes the LINK_DOWN event.
+     *
+     * @param affectedLink Link that is going down
+     */
+    protected void processLinkDown(Link affectedLink) {
+        VlanId assignedVlan = assignedVlan();
+
+        getAffectedGroups(affectedLink).forEach(mcastIp -> {
+            // Find out the ingress, transit and egress device of affected group
+            DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                    .stream().findAny().orElse(null);
+            DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+                    .stream().findAny().orElse(null);
+            Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+            if (ingressDevice == null || transitDevice == null || egressDevices == null) {
+                log.warn("Missing ingress {}, transit {}, or egress {} devices",
+                        ingressDevice, transitDevice, egressDevices);
+                return;
+            }
+
+            // Remove entire transit
+            removeGroupFromDevice(transitDevice, mcastIp, assignedVlan);
+
+            // Remove transit-facing port on ingress device
+            PortNumber ingressTransitPort = ingressTransitPort(mcastIp);
+            if (ingressTransitPort != null) {
+                removePortFromDevice(ingressDevice, ingressTransitPort, mcastIp, assignedVlan);
+                mcastRoleStore.remove(new McastStoreKey(mcastIp, transitDevice));
+            }
+
+            // Construct a new path for each egress device
+            egressDevices.forEach(egressDevice -> {
+                Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+                if (mcastPath.isPresent()) {
+                    List<Link> links = mcastPath.get().links();
+                    links.forEach(link -> {
+                        addPortToDevice(link.src().deviceId(), link.src().port(), mcastIp, assignedVlan);
+                        addFilterToDevice(link.dst().deviceId(), link.dst().port(), assignedVlan);
+                    });
+                    // Setup new transit mcast role
+                    mcastRoleStore.put(new McastStoreKey(mcastIp,
+                            links.get(0).dst().deviceId()), McastRole.TRANSIT);
+                } else {
+                    log.warn("Fail to recover egress device {} from link failure {}",
+                            egressDevice, affectedLink);
+                    removeGroupFromDevice(egressDevice, mcastIp, assignedVlan);
+                }
+            });
+        });
+    }
+
+    /**
      * Adds filtering objective for given device and port.
      *
      * @param deviceId device ID
@@ -228,7 +345,8 @@
     private void addFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan) {
         // Do nothing if the port is configured as suppressed
         ConnectPoint connectPt = new ConnectPoint(deviceId, port);
-        if (srManager.deviceConfiguration.suppressSubnet().contains(connectPt) ||
+        if (srManager.deviceConfiguration == null ||
+                srManager.deviceConfiguration.suppressSubnet().contains(connectPt) ||
                 srManager.deviceConfiguration.suppressHost().contains(connectPt)) {
             log.info("Ignore suppressed port {}", connectPt);
             return;
@@ -236,8 +354,13 @@
 
         FilteringObjective.Builder filtObjBuilder =
                 filterObjBuilder(deviceId, port, assignedVlan);
-        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.add());
-        // TODO add objective context
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully add filter on {}/{}, vlan {}",
+                        deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to add filter on {}/{}, vlan {}: {}",
+                                deviceId, port.toLong(), assignedVlan, error));
+        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.add(context));
     }
 
     /**
@@ -251,17 +374,14 @@
      */
     private void addPortToDevice(DeviceId deviceId, PortNumber port,
             IpAddress mcastIp, VlanId assignedVlan) {
-        log.info("Add port {} to {}. mcastIp={}, assignedVlan={}",
-                port, deviceId, mcastIp, assignedVlan);
-        McastNextObjectiveStoreKey mcastNextObjectiveStoreKey =
-                new McastNextObjectiveStoreKey(mcastIp, deviceId);
+        McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, deviceId);
         ImmutableSet.Builder<PortNumber> portBuilder = ImmutableSet.builder();
-        if (!mcastNextObjStore.containsKey(mcastNextObjectiveStoreKey)) {
+        if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
             // First time someone request this mcast group via this device
             portBuilder.add(port);
         } else {
             // This device already serves some subscribers of this mcast group
-            NextObjective nextObj = mcastNextObjStore.get(mcastNextObjectiveStoreKey).value();
+            NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
             // Stop if the port is already in the nextobj
             Set<PortNumber> existingPorts = getPorts(nextObj.next());
             if (existingPorts.contains(port)) {
@@ -271,14 +391,19 @@
             portBuilder.addAll(existingPorts).add(port).build();
         }
         // Create, store and apply the new nextObj and fwdObj
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully add {} on {}/{}, vlan {}",
+                        mcastIp, deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to add {} on {}/{}, vlan {}: {}",
+                                mcastIp, deviceId, port.toLong(), assignedVlan, error));
         NextObjective newNextObj =
                 nextObjBuilder(mcastIp, assignedVlan, portBuilder.build()).add();
         ForwardingObjective fwdObj =
-                fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add();
-        mcastNextObjStore.put(mcastNextObjectiveStoreKey, newNextObj);
+                fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add(context);
+        mcastNextObjStore.put(mcastStoreKey, newNextObj);
         srManager.flowObjectiveService.next(deviceId, newNextObj);
         srManager.flowObjectiveService.forward(deviceId, fwdObj);
-        // TODO add objective callback
     }
 
     /**
@@ -294,19 +419,17 @@
      */
     private boolean removePortFromDevice(DeviceId deviceId, PortNumber port,
             IpAddress mcastIp, VlanId assignedVlan) {
-        log.info("Remove port {} from {}. mcastIp={}, assignedVlan={}",
-                port, deviceId, mcastIp, assignedVlan);
-        McastNextObjectiveStoreKey mcastNextObjectiveStoreKey =
-                new McastNextObjectiveStoreKey(mcastIp, deviceId);
+        McastStoreKey mcastStoreKey =
+                new McastStoreKey(mcastIp, deviceId);
         // This device is not serving this multicast group
-        if (!mcastNextObjStore.containsKey(mcastNextObjectiveStoreKey)) {
+        if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
             log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
             return false;
         }
-        NextObjective nextObj = mcastNextObjStore.get(mcastNextObjectiveStoreKey).value();
+        NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
 
         Set<PortNumber> existingPorts = getPorts(nextObj.next());
-        // This device does not serve this multicast group
+        // This port does not serve this multicast group
         if (!existingPorts.contains(port)) {
             log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
             return false;
@@ -322,22 +445,91 @@
             // NOTE: Rely on GroupStore garbage collection rather than explicitly
             //       remove L3MG since there might be other flows/groups refer to
             //       the same L2IG
-            fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove();
-            mcastNextObjStore.remove(mcastNextObjectiveStoreKey);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("Successfully remove {} on {}/{}, vlan {}",
+                            mcastIp, deviceId, port.toLong(), assignedVlan),
+                    (objective, error) ->
+                            log.warn("Failed to remove {} on {}/{}, vlan {}: {}",
+                                    mcastIp, deviceId, port.toLong(), assignedVlan, error));
+            fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove(context);
+            mcastNextObjStore.remove(mcastStoreKey);
             srManager.flowObjectiveService.forward(deviceId, fwdObj);
         } else {
             // If this is not the last sink, update flows and groups
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("Successfully update {} on {}/{}, vlan {}",
+                            mcastIp, deviceId, port.toLong(), assignedVlan),
+                    (objective, error) ->
+                            log.warn("Failed to update {} on {}/{}, vlan {}: {}",
+                                    mcastIp, deviceId, port.toLong(), assignedVlan, error));
             newNextObj = nextObjBuilder(mcastIp, assignedVlan, existingPorts).add();
             fwdObj = fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add();
-            mcastNextObjStore.put(mcastNextObjectiveStoreKey, newNextObj);
+            mcastNextObjStore.put(mcastStoreKey, newNextObj);
             srManager.flowObjectiveService.next(deviceId, newNextObj);
             srManager.flowObjectiveService.forward(deviceId, fwdObj);
         }
-        // TODO add objective callback
-
         return existingPorts.isEmpty();
     }
 
+
+    /**
+     * Removes entire group on given device.
+     *
+     * @param deviceId device ID
+     * @param mcastIp multicast group to be removed
+     * @param assignedVlan assigned VLAN ID
+     */
+    private void removeGroupFromDevice(DeviceId deviceId, IpAddress mcastIp,
+            VlanId assignedVlan) {
+        McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, deviceId);
+        // This device is not serving this multicast group
+        if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+            log.warn("{} is not serving {}. Abort.", deviceId, mcastIp);
+            return;
+        }
+        NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+        // NOTE: Rely on GroupStore garbage collection rather than explicitly
+        //       remove L3MG since there might be other flows/groups refer to
+        //       the same L2IG
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully remove {} on {}, vlan {}",
+                        mcastIp, deviceId, assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to remove {} on {}, vlan {}: {}",
+                                mcastIp, deviceId, assignedVlan, error));
+        ForwardingObjective fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove(context);
+        srManager.flowObjectiveService.forward(deviceId, fwdObj);
+        mcastNextObjStore.remove(mcastStoreKey);
+        mcastRoleStore.remove(mcastStoreKey);
+    }
+
+    /**
+     * Remove all groups on given device.
+     *
+     * @param deviceId device ID
+     */
+    public void removeDevice(DeviceId deviceId) {
+        Iterator<Map.Entry<McastStoreKey, Versioned<NextObjective>>> itNextObj =
+                mcastNextObjStore.entrySet().iterator();
+        while (itNextObj.hasNext()) {
+            Map.Entry<McastStoreKey, Versioned<NextObjective>> entry = itNextObj.next();
+            if (entry.getKey().deviceId().equals(deviceId)) {
+                removeGroupFromDevice(entry.getKey().deviceId(), entry.getKey().mcastIp(), assignedVlan());
+                itNextObj.remove();
+            }
+        }
+
+        Iterator<Map.Entry<McastStoreKey, Versioned<McastRole>>> itRole =
+                mcastRoleStore.entrySet().iterator();
+        while (itRole.hasNext()) {
+            Map.Entry<McastStoreKey, Versioned<McastRole>> entry = itRole.next();
+            if (entry.getKey().deviceId().equals(deviceId)) {
+                itRole.remove();
+            }
+        }
+
+    }
+
     /**
      * Creates a next objective builder for multicast.
      *
@@ -456,16 +648,15 @@
     private Optional<Path> getPath(DeviceId src, DeviceId dst, IpAddress mcastIp) {
         List<Path> allPaths = Lists.newArrayList(
                 topologyService.getPaths(topologyService.currentTopology(), src, dst));
+        log.debug("{} path(s) found from {} to {}", allPaths.size(), src, dst);
         if (allPaths.isEmpty()) {
-            log.warn("Fail to find a path from {} to {}. Abort.", src, dst);
             return Optional.empty();
         }
 
         // If one of the available path is used before, use the same path
-        McastNextObjectiveStoreKey mcastNextObjectiveStoreKey =
-                new McastNextObjectiveStoreKey(mcastIp, src);
-        if (mcastNextObjStore.containsKey(mcastNextObjectiveStoreKey)) {
-            NextObjective nextObj = mcastNextObjStore.get(mcastNextObjectiveStoreKey).value();
+        McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, src);
+        if (mcastNextObjStore.containsKey(mcastStoreKey)) {
+            NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
             Set<PortNumber> existingPorts = getPorts(nextObj.next());
             for (Path path : allPaths) {
                 PortNumber srcPort = path.links().get(0).src().port();
@@ -480,6 +671,37 @@
     }
 
     /**
+     * Gets device(s) of given role in given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @param role multicast role
+     * @return set of device ID or empty set if not found
+     */
+    private Set<DeviceId> getDevice(IpAddress mcastIp, McastRole role) {
+        return mcastRoleStore.entrySet().stream()
+                .filter(entry -> entry.getKey().mcastIp().equals(mcastIp) &&
+                        entry.getValue().value() == role)
+                .map(Map.Entry::getKey).map(McastStoreKey::deviceId)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Gets groups which is affected by the link down event.
+     *
+     * @param link link going down
+     * @return a set of multicast IpAddress
+     */
+    private Set<IpAddress> getAffectedGroups(Link link) {
+        DeviceId deviceId = link.src().deviceId();
+        PortNumber port = link.src().port();
+        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)
+                .collect(Collectors.toSet());
+    }
+
+    /**
      * Gets egress VLAN from McastConfig.
      *
      * @return egress VLAN or VlanId.NONE if not configured
@@ -500,4 +722,34 @@
                 VlanId.vlanId(SegmentRoutingManager.ASSIGNED_VLAN_NO_SUBNET) :
                 egressVlan();
     }
+
+    /**
+     * Gets the spine-facing port on ingress device of given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @return spine-facing port on ingress device
+     */
+    private PortNumber ingressTransitPort(IpAddress mcastIp) {
+        DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                .stream().findAny().orElse(null);
+        if (ingressDevice != null) {
+            NextObjective nextObj = mcastNextObjStore
+                    .get(new McastStoreKey(mcastIp, ingressDevice)).value();
+            Set<PortNumber> ports = getPorts(nextObj.next());
+
+            for (PortNumber port : ports) {
+                // Spine-facing port should have no subnet and no xconnect
+                if (srManager.deviceConfiguration != null &&
+                        srManager.deviceConfiguration.getPortSubnet(ingressDevice, port) == null &&
+                        srManager.deviceConfiguration.getXConnects().values().stream()
+                                .allMatch(connectPoints ->
+                                        connectPoints.stream().noneMatch(connectPoint ->
+                                                connectPoint.port().equals(port))
+                                )) {
+                    return port;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index 48bb1a7..5eec207 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -762,6 +762,8 @@
         defaultRoutingHandler.populateRoutingRulesForLinkStatusChange(null);
         //log.trace("processLinkAdded: re-starting route population process");
         //defaultRoutingHandler.startPopulationProcess();
+
+        mcastHandler.init();
     }
 
     private void processLinkRemoved(Link link) {
@@ -775,6 +777,8 @@
         defaultRoutingHandler.populateRoutingRulesForLinkStatusChange(link);
         //log.trace("processLinkRemoved: re-starting route population process");
         //defaultRoutingHandler.startPopulationProcess();
+
+        mcastHandler.processLinkDown(link);
     }
 
     private void processDeviceAdded(Device device) {
@@ -784,43 +788,50 @@
                     + "processed after config completes.", device.id());
             return;
         }
+        processDeviceAddedInternal(device.id());
+    }
+
+    private void processDeviceAddedInternal(DeviceId deviceId) {
         // Irrespective of whether the local is a MASTER or not for this device,
         // we need to create a SR-group-handler instance. This is because in a
         // multi-instance setup, any instance can initiate forwarding/next-objectives
         // for any switch (even if this instance is a SLAVE or not even connected
         // to the switch). To handle this, a default-group-handler instance is necessary
         // per switch.
-        if (groupHandlerMap.get(device.id()) == null) {
+        log.debug("Current groupHandlerMap devs: {}", groupHandlerMap.keySet());
+        if (groupHandlerMap.get(deviceId) == null) {
             DefaultGroupHandler groupHandler;
             try {
                 groupHandler = DefaultGroupHandler.
-                        createGroupHandler(device.id(),
-                                           appId,
-                                           deviceConfiguration,
-                                           linkService,
-                                           flowObjectiveService,
-                                           this);
+                        createGroupHandler(deviceId,
+                                appId,
+                                deviceConfiguration,
+                                linkService,
+                                flowObjectiveService,
+                                this);
             } catch (DeviceConfigNotFoundException e) {
                 log.warn(e.getMessage() + " Aborting processDeviceAdded.");
                 return;
             }
-            groupHandlerMap.put(device.id(), groupHandler);
+            log.debug("updating groupHandlerMap with new config for device: {}",
+                    deviceId);
+            groupHandlerMap.put(deviceId, groupHandler);
             // Also, in some cases, drivers may need extra
             // information to process rules (eg. Router IP/MAC); and so, we send
             // port addressing rules to the driver as well irrespective of whether
             // this instance is the master or not.
-            defaultRoutingHandler.populatePortAddressingRules(device.id());
+            defaultRoutingHandler.populatePortAddressingRules(deviceId);
         }
-        if (mastershipService.isLocalMaster(device.id())) {
-            hostHandler.readInitialHosts(device.id());
-            DefaultGroupHandler groupHandler = groupHandlerMap.get(device.id());
+        if (mastershipService.isLocalMaster(deviceId)) {
+            hostHandler.readInitialHosts(deviceId);
+            DefaultGroupHandler groupHandler = groupHandlerMap.get(deviceId);
             groupHandler.createGroupsFromSubnetConfig();
-            routingRulePopulator.populateSubnetBroadcastRule(device.id());
-            groupHandler.createGroupsForXConnect(device.id());
-            routingRulePopulator.populateXConnectBroadcastRule(device.id());
+            routingRulePopulator.populateSubnetBroadcastRule(deviceId);
+            groupHandler.createGroupsForXConnect(deviceId);
+            routingRulePopulator.populateXConnectBroadcastRule(deviceId);
         }
 
-        netcfgHandler.initVRouters(device.id());
+        netcfgHandler.initVRouters(deviceId);
     }
 
     private void processDeviceRemoved(Device device) {
@@ -829,34 +840,29 @@
                 .forEach(entry -> {
                     nsNextObjStore.remove(entry.getKey());
                 });
-
         subnetNextObjStore.entrySet().stream()
                 .filter(entry -> entry.getKey().deviceId().equals(device.id()))
                 .forEach(entry -> {
                     subnetNextObjStore.remove(entry.getKey());
                 });
-
         portNextObjStore.entrySet().stream()
                 .filter(entry -> entry.getKey().deviceId().equals(device.id()))
                 .forEach(entry -> {
                     portNextObjStore.remove(entry.getKey());
                 });
-
         xConnectNextObjStore.entrySet().stream()
                 .filter(entry -> entry.getKey().deviceId().equals(device.id()))
                 .forEach(entry -> {
                     xConnectNextObjStore.remove(entry.getKey());
                 });
-
         subnetVidStore.entrySet().stream()
                 .filter(entry -> entry.getKey().deviceId().equals(device.id()))
                 .forEach(entry -> {
                     subnetVidStore.remove(entry.getKey());
                 });
-
         groupHandlerMap.remove(device.id());
-
         defaultRoutingHandler.purgeEcmpGraph(device.id());
+        mcastHandler.removeDevice(device.id());
     }
 
     private void processPortRemoved(Device device, Port port) {
@@ -900,48 +906,11 @@
                                               tunnelHandler, policyStore);
 
             for (Device device : deviceService.getDevices()) {
-                // Irrespective of whether the local is a MASTER or not for this device,
-                // we need to create a SR-group-handler instance. This is because in a
-                // multi-instance setup, any instance can initiate forwarding/next-objectives
-                // for any switch (even if this instance is a SLAVE or not even connected
-                // to the switch). To handle this, a default-group-handler instance is necessary
-                // per switch.
-                log.debug("Current groupHandlerMap devs: {}", groupHandlerMap.keySet());
-                if (groupHandlerMap.get(device.id()) == null) {
-                    DefaultGroupHandler groupHandler;
-                    try {
-                        groupHandler = DefaultGroupHandler.
-                                createGroupHandler(device.id(),
-                                                   appId,
-                                                   deviceConfiguration,
-                                                   linkService,
-                                                   flowObjectiveService,
-                                                   segmentRoutingManager);
-                    } catch (DeviceConfigNotFoundException e) {
-                        log.warn(e.getMessage() + " Aborting configureNetwork.");
-                        return;
-                    }
-                    log.debug("updating groupHandlerMap with new config for "
-                            + "device: {}", device.id());
-                    groupHandlerMap.put(device.id(), groupHandler);
-
-                    // Also, in some cases, drivers may need extra
-                    // information to process rules (eg. Router IP/MAC); and so, we send
-                    // port addressing rules to the driver as well, irrespective of whether
-                    // this instance is the master or not.
-                    defaultRoutingHandler.populatePortAddressingRules(device.id());
-                }
-                if (mastershipService.isLocalMaster(device.id())) {
-                    hostHandler.readInitialHosts(device.id());
-                    DefaultGroupHandler groupHandler = groupHandlerMap.get(device.id());
-                    groupHandler.createGroupsFromSubnetConfig();
-                    routingRulePopulator.populateSubnetBroadcastRule(device.id());
-                    groupHandler.createGroupsForXConnect(device.id());
-                    routingRulePopulator.populateXConnectBroadcastRule(device.id());
-                }
+                processDeviceAddedInternal(device.id());
             }
 
             defaultRoutingHandler.startPopulationProcess();
+            mcastHandler.init();
         }
 
         @Override
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/storekey/McastNextObjectiveStoreKey.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
similarity index 89%
rename from apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/storekey/McastNextObjectiveStoreKey.java
rename to apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
index 8fed202..5cf6906 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/storekey/McastNextObjectiveStoreKey.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
@@ -26,7 +26,7 @@
 /**
  * Key of multicast next objective store.
  */
-public class McastNextObjectiveStoreKey {
+public class McastStoreKey {
     private final IpAddress mcastIp;
     private final DeviceId deviceId;
 
@@ -36,7 +36,7 @@
      * @param mcastIp multicast group IP address
      * @param deviceId device ID
      */
-    public McastNextObjectiveStoreKey(IpAddress mcastIp, DeviceId deviceId) {
+    public McastStoreKey(IpAddress mcastIp, DeviceId deviceId) {
         checkNotNull(mcastIp, "mcastIp cannot be null");
         checkNotNull(deviceId, "deviceId cannot be null");
         checkArgument(mcastIp.isMulticast(), "mcastIp must be a multicast address");
@@ -67,11 +67,11 @@
         if (this == o) {
             return true;
         }
-        if (!(o instanceof McastNextObjectiveStoreKey)) {
+        if (!(o instanceof McastStoreKey)) {
             return false;
         }
-        McastNextObjectiveStoreKey that =
-                (McastNextObjectiveStoreKey) o;
+        McastStoreKey that =
+                (McastStoreKey) o;
         return (Objects.equals(this.mcastIp, that.mcastIp) &&
                 Objects.equals(this.deviceId, that.deviceId));
     }
diff --git a/tools/package/config/samples/network-cfg-fabric-2x2-all-readme.html b/tools/package/config/samples/network-cfg-fabric-2x2-all-readme.html
index bcbc039..4e76263 100644
--- a/tools/package/config/samples/network-cfg-fabric-2x2-all-readme.html
+++ b/tools/package/config/samples/network-cfg-fabric-2x2-all-readme.html
@@ -5,7 +5,7 @@
   <meta http-equiv="Content-Style-Type" content="text/css">
   <title></title>
   <meta name="Generator" content="Cocoa HTML Writer">
-  <meta name="CocoaVersion" content="1404.34">
+  <meta name="CocoaVersion" content="1404.46">
   <style type="text/css">
     p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Menlo; color: #0433ff; -webkit-text-stroke: #0433ff}
     p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Menlo; color: #ff9300; -webkit-text-stroke: #ff9300}
@@ -14,6 +14,7 @@
     p.p5 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Menlo; color: #00c7fc; -webkit-text-stroke: #00c7fc}
     p.p6 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Menlo; color: #ff2600; -webkit-text-stroke: #ff2600}
     p.p7 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Menlo; color: #000000; -webkit-text-stroke: #000000}
+    p.p8 {margin: 0.0px 0.0px 0.0px 0.0px; line-height: 14.0px; font: 12.0px Menlo; color: #00c7fc; -webkit-text-stroke: #000000}
     span.s1 {font-kerning: none}
     span.s2 {font-kerning: none; color: #0433ff; -webkit-text-stroke: 0px #0433ff}
     span.s3 {font-kerning: none; color: #000000; -webkit-text-stroke: 0px #000000}
@@ -22,7 +23,8 @@
     span.s6 {font-kerning: none; color: #77bb41; -webkit-text-stroke: 0px #77bb41}
     span.s7 {font-kerning: none; color: #00c7fc; -webkit-text-stroke: 0px #00c7fc}
     span.s8 {font-kerning: none; color: #ff2600; -webkit-text-stroke: 0px #ff2600}
-    span.s9 {font-kerning: none; color: #669c35; -webkit-text-stroke: 0px #669c35}
+    span.s9 {font-kerning: none; color: #000000}
+    span.s10 {font-kerning: none; color: #669c35; -webkit-text-stroke: 0px #669c35}
     span.Apple-tab-span {white-space:pre}
   </style>
 </head>
@@ -31,7 +33,7 @@
 <p class="p2"><span class="s1">Orange: vSG LAN connectivity (cross-connect)</span></p>
 <p class="p3"><span class="s1">Magenta: vSG WAN connectivity (default route)</span></p>
 <p class="p4"><span class="s1">Green: vRouter integration</span></p>
-<p class="p5"><span class="s1">Light Blue: PIM integration</span></p>
+<p class="p5"><span class="s1">Light Blue: Multicast</span></p>
 <p class="p6"><span class="s1">Red: Link validation</span></p>
 <p class="p7"><span class="s1"><br>
 </span></p>
@@ -221,7 +223,11 @@
 <p class="p7"><span class="s1"><span class="Apple-converted-space">        </span>"org.onosproject.core" : {</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">            </span>"core" : {</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"linkDiscoveryMode" : "STRICT" </span><span class="s8">// enable strict link validation</span></p>
-<p class="p7"><span class="s1"><span class="Apple-converted-space">            </span>} <span class="Apple-converted-space">   </span></span></p>
+<p class="p7"><span class="s1"><span class="Apple-converted-space">            </span>},</span></p>
+<p class="p8"><span class="s9"><span class="Apple-tab-span">	</span><span class="Apple-tab-span">	</span><span class="Apple-converted-space">  </span>"multicast": { </span><span class="s1">// The VLAN we expect to see on the multicast ingress and egress</span></p>
+<p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"ingressVlan": "None",</span></p>
+<p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"egressVlan": "None"</span></p>
+<p class="p7"><span class="s1"><span class="Apple-converted-space">            </span>}<span class="Apple-converted-space"> </span></span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">        </span>},</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">        </span>"org.onosproject.segmentrouting" : {</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">            </span>"segmentrouting" : {</span></p>
@@ -243,7 +249,7 @@
 <p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"controlPlaneConnectPoint" : "of:0000000000000002/31", </span><span class="s6">// location of Quagga</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"ospfEnabled" : "true", </span><span class="s6">// enable OSPF</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"pimEnabled" : "true", </span><span class="s7">// enable PIM</span></p>
-<p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"interfaces" : [ "external-quagga" ] </span><span class="s9">// </span><span class="s6">VR only handles peers on these ports</span></p>
+<p class="p7"><span class="s1"><span class="Apple-converted-space">                </span>"interfaces" : [ "external-quagga" ] </span><span class="s10">// </span><span class="s6">VR only handles peers on these ports</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">            </span>}</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">        </span>}</span></p>
 <p class="p7"><span class="s1"><span class="Apple-converted-space">    </span>}</span></p>
diff --git a/tools/package/config/samples/network-cfg-fabric-2x2-all.json b/tools/package/config/samples/network-cfg-fabric-2x2-all.json
index f21272e..5b6f036 100644
--- a/tools/package/config/samples/network-cfg-fabric-2x2-all.json
+++ b/tools/package/config/samples/network-cfg-fabric-2x2-all.json
@@ -228,6 +228,10 @@
         "org.onosproject.core" : {
             "core" : {
                 "linkDiscoveryMode" : "STRICT"
+            },
+            "multicast": {
+                "ingressVlan": "None",
+                "egressVlan": "None"
             }
         },
         "org.onosproject.segmentrouting" : {