CORD-348 multicast support in SegmentRouting and vRouter

In this submission:
* Setup/teardown multicast route according to SinkAdded/SinkRemoved event
    - ingressVlan and egressVlan is configurable through network config
* Change behavior of OFDPA VLAN assignment
    - Always use the VLAN in metadata if present
* Bugfix of writing immutable object

NOT in this submission (coming soon):
* Error handling (e.g. link/device failure recovery)

Change-Id: I9be11af04eb2d6456b865c7e59e96cc02370f846
diff --git a/apps/routing/src/main/java/org/onosproject/routing/impl/SingleSwitchFibInstaller.java b/apps/routing/src/main/java/org/onosproject/routing/impl/SingleSwitchFibInstaller.java
index 473450d..f0649b9 100644
--- a/apps/routing/src/main/java/org/onosproject/routing/impl/SingleSwitchFibInstaller.java
+++ b/apps/routing/src/main/java/org/onosproject/routing/impl/SingleSwitchFibInstaller.java
@@ -29,11 +29,13 @@
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 import org.onlab.util.Tools;
 import org.onosproject.cfg.ComponentConfigService;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.config.basics.McastConfig;
 import org.onosproject.incubator.net.intf.Interface;
 import org.onosproject.incubator.net.intf.InterfaceEvent;
 import org.onosproject.incubator.net.intf.InterfaceListener;
@@ -44,9 +46,12 @@
 import org.onosproject.incubator.net.routing.RouteService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.ConfigFactory;
 import org.onosproject.net.config.NetworkConfigEvent;
 import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
 import org.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.config.basics.SubjectFactories;
 import org.onosproject.net.device.DeviceEvent;
 import org.onosproject.net.device.DeviceListener;
 import org.onosproject.net.device.DeviceService;
@@ -102,6 +107,9 @@
     protected NetworkConfigService networkConfigService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry networkConfigRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected ComponentConfigService componentConfigService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -123,6 +131,7 @@
 
     private List<String> interfaces;
 
+    private ApplicationId coreAppId;
     private ApplicationId routerAppId;
 
     // Reference count for how many times a next hop is used by a route
@@ -138,13 +147,25 @@
     private InternalInterfaceListener internalInterfaceList = new InternalInterfaceListener();
     private InternalRouteListener routeListener = new InternalRouteListener();
 
+    private ConfigFactory<ApplicationId, McastConfig> mcastConfigFactory =
+            new ConfigFactory<ApplicationId, McastConfig>(SubjectFactories.APP_SUBJECT_FACTORY,
+                    McastConfig.class, "multicast") {
+                @Override
+                public McastConfig createConfig() {
+                    return new McastConfig();
+                }
+            };
+
     @Activate
     protected void activate(ComponentContext context) {
         componentConfigService.registerProperties(getClass());
         modified(context);
 
+        coreAppId = coreService.registerApplication(CoreService.CORE_APP_NAME);
         routerAppId = coreService.registerApplication(RoutingService.ROUTER_APP_ID);
 
+        networkConfigRegistry.registerConfigFactory(mcastConfigFactory);
+
         deviceListener = new InternalDeviceListener();
         deviceService.addListener(deviceListener);
 
@@ -368,6 +389,7 @@
             }
 
             createFilteringObjective(install, intf);
+            createMcastFilteringObjective(install, intf);
         }
     }
 
@@ -380,10 +402,14 @@
         }
 
         createFilteringObjective(install, intf);
+        createMcastFilteringObjective(install, intf);
     }
 
     //create filtering objective for interface
     private void createFilteringObjective(boolean install, Interface intf) {
+        VlanId assignedVlan = (egressVlan().equals(VlanId.NONE)) ?
+                VlanId.vlanId(ASSIGNED_VLAN) :
+                egressVlan();
 
         FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
         // first add filter for the interface
@@ -393,12 +419,12 @@
         fob.withPriority(PRIORITY_OFFSET);
         if (intf.vlan() == VlanId.NONE) {
             TrafficTreatment tt = DefaultTrafficTreatment.builder()
-                    .pushVlan().setVlanId(VlanId.vlanId(ASSIGNED_VLAN)).build();
+                    .pushVlan().setVlanId(assignedVlan).build();
             fob.withMeta(tt);
         }
-
         fob.permit().fromApp(routerAppId);
         sendFilteringObjective(install, fob, intf);
+
         if (controlPlaneConnectPoint != null) {
             // then add the same mac/vlan filters for control-plane connect point
             fob.withKey(Criteria.matchInPort(controlPlaneConnectPoint.port()));
@@ -406,6 +432,27 @@
         }
     }
 
+    //create filtering objective for multicast traffic
+    private void createMcastFilteringObjective(boolean install, Interface intf) {
+        VlanId assignedVlan = (egressVlan().equals(VlanId.NONE)) ?
+                VlanId.vlanId(ASSIGNED_VLAN) :
+                egressVlan();
+
+        FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+        // first add filter for the interface
+        fob.withKey(Criteria.matchInPort(intf.connectPoint().port()))
+                .addCondition(Criteria.matchEthDstMasked(MacAddress.IPV4_MULTICAST,
+                        MacAddress.IPV4_MULTICAST_MASK))
+                .addCondition(Criteria.matchVlanId(ingressVlan()));
+        fob.withPriority(PRIORITY_OFFSET);
+        TrafficTreatment tt = DefaultTrafficTreatment.builder()
+                .pushVlan().setVlanId(assignedVlan).build();
+        fob.withMeta(tt);
+
+        fob.permit().fromApp(routerAppId);
+        sendFilteringObjective(install, fob, intf);
+    }
+
     private void sendFilteringObjective(boolean install, FilteringObjective.Builder fob,
                                         Interface intf) {
 
@@ -419,6 +466,18 @@
         flowObjectiveService.filter(deviceId, filter);
     }
 
+    private VlanId ingressVlan() {
+        McastConfig mcastConfig =
+                networkConfigService.getConfig(coreAppId, McastConfig.class);
+        return (mcastConfig != null) ? mcastConfig.ingressVlan() : VlanId.NONE;
+    }
+
+    private VlanId egressVlan() {
+        McastConfig mcastConfig =
+                networkConfigService.getConfig(coreAppId, McastConfig.class);
+        return (mcastConfig != null) ? mcastConfig.egressVlan() : VlanId.NONE;
+    }
+
     private class InternalRouteListener implements RouteListener {
         @Override
         public void event(RouteEvent event) {
@@ -490,7 +549,6 @@
     }
 
     private class InternalInterfaceListener implements InterfaceListener {
-
         @Override
         public void event(InterfaceEvent event) {
             Interface intf = event.subject();
diff --git a/apps/routing/src/test/java/org/onosproject/routing/impl/SingleSwitchFibInstallerTest.java b/apps/routing/src/test/java/org/onosproject/routing/impl/SingleSwitchFibInstallerTest.java
index 989b297..b1b80c5 100644
--- a/apps/routing/src/test/java/org/onosproject/routing/impl/SingleSwitchFibInstallerTest.java
+++ b/apps/routing/src/test/java/org/onosproject/routing/impl/SingleSwitchFibInstallerTest.java
@@ -40,6 +40,7 @@
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigRegistry;
 import org.onosproject.net.config.NetworkConfigService;
 import org.onosproject.net.device.DeviceListener;
 import org.onosproject.net.device.DeviceService;
@@ -105,6 +106,7 @@
     private final Set<Interface> interfaces = Sets.newHashSet();
     private InterfaceService interfaceService;
     private NetworkConfigService networkConfigService;
+    private NetworkConfigRegistry networkConfigRegistry;
     private FlowObjectiveService flowObjectiveService;
     private DeviceService deviceService;
     private static final ApplicationId APPID = TestApplicationId.create("foo");
@@ -128,13 +130,15 @@
         interfaceService = createMock(InterfaceService.class);
 
         networkConfigService = createMock(NetworkConfigService.class);
+        networkConfigRegistry = createMock(NetworkConfigRegistry.class);
         flowObjectiveService = createMock(FlowObjectiveService.class);
         deviceService = new TestDeviceService();
         CoreService coreService = createNiceMock(CoreService.class);
-        expect(coreService.registerApplication(anyString())).andReturn(APPID);
+        expect(coreService.registerApplication(anyString())).andReturn(APPID).anyTimes();
         replay(coreService);
 
         sSfibInstaller.networkConfigService = networkConfigService;
+        sSfibInstaller.networkConfigRegistry = networkConfigRegistry;
         sSfibInstaller.interfaceService = interfaceService;
         sSfibInstaller.flowObjectiveService = flowObjectiveService;
         sSfibInstaller.coreService = coreService;
diff --git a/apps/segmentrouting/BUCK b/apps/segmentrouting/BUCK
index 68b76d0..f532bea 100644
--- a/apps/segmentrouting/BUCK
+++ b/apps/segmentrouting/BUCK
@@ -3,9 +3,9 @@
     '//lib:org.apache.karaf.shell.console',
     '//lib:javax.ws.rs-api',
     '//cli:onos-cli',
+    '//core/store/serializers:onos-core-serializers',
     '//incubator/api:onos-incubator-api',
     '//utils/rest:onlab-rest',
-    '//core/store/serializers:onos-core-serializers',
 ]
 
 TEST_DEPS = [
diff --git a/apps/segmentrouting/pom.xml b/apps/segmentrouting/pom.xml
index beff610..b2db87f 100644
--- a/apps/segmentrouting/pom.xml
+++ b/apps/segmentrouting/pom.xml
@@ -51,7 +51,11 @@
             <artifactId>onos-cli</artifactId>
             <version>${project.version}</version>
         </dependency>
-
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.karaf.shell</groupId>
             <artifactId>org.apache.karaf.shell.console</artifactId>
@@ -84,12 +88,10 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
         </dependency>
-
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
         </dependency>
-
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.compendium</artifactId>
@@ -103,7 +105,6 @@
             <artifactId>onlab-junit</artifactId>
             <scope>test</scope>
         </dependency>
-
         <dependency>
             <groupId>org.onosproject</groupId>
             <artifactId>onos-api</artifactId>
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastEventHandler.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastEventHandler.java
new file mode 100644
index 0000000..928116a2
--- /dev/null
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/McastEventHandler.java
@@ -0,0 +1,503 @@
+/*
+ * 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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.config.basics.McastConfig;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.mcast.McastEvent;
+import org.onosproject.net.mcast.McastRouteInfo;
+import org.onosproject.net.topology.TopologyService;
+import org.onosproject.segmentrouting.grouphandler.McastNextObjectiveStoreKey;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Multicast event handler.
+ */
+public class McastEventHandler {
+    private static final Logger log = LoggerFactory.getLogger(McastEventHandler.class);
+    private final SegmentRoutingManager srManager;
+    private final ApplicationId coreAppId;
+    private StorageService storageService;
+    private TopologyService topologyService;
+    private final KryoNamespace.Builder kryoBuilder;
+    private final ConsistentMap<McastNextObjectiveStoreKey, NextObjective> mcastNextObjStore;
+
+    /**
+     * Constructs the McastEventHandler.
+     *
+     * @param srManager Segment Routing manager
+     */
+    public McastEventHandler(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()
+                .register(KryoNamespaces.API)
+                .register(McastNextObjectiveStoreKey.class);
+        mcastNextObjStore = storageService
+                .<McastNextObjectiveStoreKey, NextObjective>consistentMapBuilder()
+                .withName("onos-mcast-nextobj-store")
+                .withSerializer(Serializer.using(kryoBuilder.build()))
+                .build();
+    }
+
+    /**
+     * Processes the SOURCE_ADDED event.
+     *
+     * @param event McastEvent with SOURCE_ADDED type
+     */
+    protected void processSourceAdded(McastEvent event) {
+        log.info("processSourceAdded {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.isComplete()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        ConnectPoint source = mcastRouteInfo.source().orElse(null);
+        Set<ConnectPoint> sinks = mcastRouteInfo.sinks();
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+
+        sinks.forEach(sink -> {
+            processSinkAddedInternal(source, sink, mcastIp);
+        });
+    }
+
+    /**
+     * Processes the SINK_ADDED event.
+     *
+     * @param event McastEvent with SINK_ADDED type
+     */
+    protected void processSinkAdded(McastEvent event) {
+        log.info("processSinkAdded {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.isComplete()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        ConnectPoint source = mcastRouteInfo.source().orElse(null);
+        ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+
+        processSinkAddedInternal(source, sink, mcastIp);
+    }
+
+    /**
+     * Processes the SINK_REMOVED event.
+     *
+     * @param event McastEvent with SINK_REMOVED type
+     */
+    protected void processSinkRemoved(McastEvent event) {
+        log.info("processSinkRemoved {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.isComplete()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        ConnectPoint source = mcastRouteInfo.source().orElse(null);
+        ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+        VlanId assignedVlan = 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.
+            if (source.port().equals(sink.port())) {
+                log.warn("Sink is on the same port of source. Abort");
+                return;
+            }
+            removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan);
+            return;
+        }
+
+        // Process the egress device
+        boolean isLast = removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan);
+
+        // If this is the last sink on the device, also update upstream
+        Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+        if (mcastPath.isPresent()) {
+            List<Link> links = Lists.newArrayList(mcastPath.get().links());
+            Collections.reverse(links);
+            for (Link link : links) {
+                if (isLast) {
+                    isLast = removePortFromDevice(link.src().deviceId(), link.src().port(),
+                            mcastIp, assignedVlan);
+                }
+            }
+        }
+    }
+
+    /**
+     * Establishes a path from source to sink for given multicast group.
+     *
+     * @param source connect point of the multicast source
+     * @param sink connection point of the multicast sink
+     * @param mcastIp multicast group IP address
+     */
+    private void processSinkAddedInternal(ConnectPoint source, ConnectPoint sink,
+            IpAddress mcastIp) {
+        VlanId assignedVlan = 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.
+            if (source.port().equals(sink.port())) {
+                log.warn("Sink is on the same port of source. Abort");
+                return;
+            }
+            addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan);
+            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 -> {
+                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);
+        }
+    }
+
+    /**
+     * Adds filtering objective for given device and port.
+     *
+     * @param deviceId device ID
+     * @param port ingress port number
+     * @param assignedVlan assigned VLAN ID
+     */
+    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) ||
+                srManager.deviceConfiguration.suppressHost().contains(connectPt)) {
+            log.info("Ignore suppressed port {}", connectPt);
+            return;
+        }
+
+        FilteringObjective.Builder filtObjBuilder =
+                filterObjBuilder(deviceId, port, assignedVlan);
+        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.add());
+        // TODO add objective context
+    }
+
+    /**
+     * Adds a port to given multicast group on given device. This involves the
+     * update of L3 multicast group and multicast routing table entry.
+     *
+     * @param deviceId device ID
+     * @param port port to be added
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     */
+    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);
+        ImmutableSet.Builder<PortNumber> portBuilder = ImmutableSet.builder();
+        if (!mcastNextObjStore.containsKey(mcastNextObjectiveStoreKey)) {
+            // 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();
+            // Stop if the port is already in the nextobj
+            Set<PortNumber> existingPorts = getPorts(nextObj.next());
+            if (existingPorts.contains(port)) {
+                log.info("NextObj for {}/{} already exists. Abort", deviceId, port);
+                return;
+            }
+            portBuilder.addAll(existingPorts).add(port).build();
+        }
+        // Create, store and apply the new nextObj and fwdObj
+        NextObjective newNextObj =
+                nextObjBuilder(mcastIp, assignedVlan, portBuilder.build()).add();
+        ForwardingObjective fwdObj =
+                fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add();
+        mcastNextObjStore.put(mcastNextObjectiveStoreKey, newNextObj);
+        srManager.flowObjectiveService.next(deviceId, newNextObj);
+        srManager.flowObjectiveService.forward(deviceId, fwdObj);
+        // TODO add objective callback
+    }
+
+    /**
+     * Removes a port from given multicast group on given device.
+     * This involves the update of L3 multicast group and multicast routing
+     * table entry.
+     *
+     * @param deviceId device ID
+     * @param port port to be added
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @return true if this is the last sink on this device
+     */
+    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);
+        // This device is not serving this multicast group
+        if (!mcastNextObjStore.containsKey(mcastNextObjectiveStoreKey)) {
+            log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
+            return false;
+        }
+        NextObjective nextObj = mcastNextObjStore.get(mcastNextObjectiveStoreKey).value();
+
+        Set<PortNumber> existingPorts = getPorts(nextObj.next());
+        // This device does not serve this multicast group
+        if (!existingPorts.contains(port)) {
+            log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
+            return false;
+        }
+        // Copy and modify the ImmutableSet
+        existingPorts = Sets.newHashSet(existingPorts);
+        existingPorts.remove(port);
+
+        NextObjective newNextObj;
+        ForwardingObjective fwdObj;
+        if (existingPorts.isEmpty()) {
+            // If this is the last sink, remove flows and groups
+            // 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);
+            srManager.flowObjectiveService.forward(deviceId, fwdObj);
+        } else {
+            // If this is not the last sink, update flows and groups
+            newNextObj = nextObjBuilder(mcastIp, assignedVlan, existingPorts).add();
+            fwdObj = fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add();
+            mcastNextObjStore.put(mcastNextObjectiveStoreKey, newNextObj);
+            srManager.flowObjectiveService.next(deviceId, newNextObj);
+            srManager.flowObjectiveService.forward(deviceId, fwdObj);
+        }
+        // TODO add objective callback
+
+        return existingPorts.isEmpty();
+    }
+
+    /**
+     * Creates a next objective builder for multicast.
+     *
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @param outPorts set of output port numbers
+     * @return next objective builder
+     */
+    private NextObjective.Builder nextObjBuilder(IpAddress mcastIp,
+            VlanId assignedVlan, Set<PortNumber> outPorts) {
+        int nextId = srManager.flowObjectiveService.allocateNextId();
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder()
+                        .matchVlanId(assignedVlan)
+                        .matchIPDst(mcastIp.toIpPrefix())
+                        .build();
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(srManager.appId)
+                .withMeta(metadata);
+
+        outPorts.forEach(port -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            if (egressVlan().equals(VlanId.NONE)) {
+                tBuilder.popVlan();
+            }
+            tBuilder.setOutput(port);
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+
+        return nextObjBuilder;
+    }
+
+    /**
+     * Creates a forwarding objective builder for multicast.
+     *
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @param nextId next ID of the L3 multicast group
+     * @return forwarding objective builder
+     */
+    private ForwardingObjective.Builder fwdObjBuilder(IpAddress mcastIp,
+            VlanId assignedVlan, int nextId) {
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        IpPrefix mcastPrefix = IpPrefix.valueOf(mcastIp, IpPrefix.MAX_INET_MASK_LENGTH);
+        sbuilder.matchEthType(Ethernet.TYPE_IPV4);
+        sbuilder.matchIPDst(mcastPrefix);
+        TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+        metabuilder.matchVlanId(assignedVlan);
+
+        ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective.builder();
+        fwdBuilder.withSelector(sbuilder.build())
+                .withMeta(metabuilder.build())
+                .nextStep(nextId)
+                .withFlag(ForwardingObjective.Flag.SPECIFIC)
+                .fromApp(srManager.appId)
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        return fwdBuilder;
+    }
+
+    /**
+     * Creates a filtering objective builder for multicast.
+     *
+     * @param deviceId Device ID
+     * @param ingressPort ingress port of the multicast stream
+     * @param assignedVlan assigned VLAN ID
+     * @return filtering objective builder
+     */
+    private FilteringObjective.Builder filterObjBuilder(DeviceId deviceId, PortNumber ingressPort,
+            VlanId assignedVlan) {
+        FilteringObjective.Builder filtBuilder = DefaultFilteringObjective.builder();
+        filtBuilder.withKey(Criteria.matchInPort(ingressPort))
+                .addCondition(Criteria.matchEthDstMasked(MacAddress.IPV4_MULTICAST,
+                        MacAddress.IPV4_MULTICAST_MASK))
+                .addCondition(Criteria.matchVlanId(egressVlan()))
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        // vlan assignment is valid only if this instance is master
+        if (srManager.mastershipService.isLocalMaster(deviceId)) {
+            TrafficTreatment tt = DefaultTrafficTreatment.builder()
+                    .pushVlan().setVlanId(assignedVlan).build();
+            filtBuilder.withMeta(tt);
+        }
+        return filtBuilder.permit().fromApp(srManager.appId);
+    }
+
+    /**
+     * Gets output ports information from treatments.
+     *
+     * @param treatments collection of traffic treatments
+     * @return set of output port numbers
+     */
+    private Set<PortNumber> getPorts(Collection<TrafficTreatment> treatments) {
+        ImmutableSet.Builder<PortNumber> builder = ImmutableSet.builder();
+        treatments.forEach(treatment -> {
+            treatment.allInstructions().stream()
+                    .filter(instr -> instr instanceof OutputInstruction)
+                    .forEach(instr -> {
+                        builder.add(((OutputInstruction) instr).port());
+                    });
+        });
+        return builder.build();
+    }
+
+    /**
+     * Gets a path from src to dst.
+     * If a path was allocated before, returns the allocated path.
+     * Otherwise, randomly pick one from available paths.
+     *
+     * @param src source device ID
+     * @param dst destination device ID
+     * @param mcastIp multicast group
+     * @return an optional path from src to dst
+     */
+    private Optional<Path> getPath(DeviceId src, DeviceId dst, IpAddress mcastIp) {
+        List<Path> allPaths = Lists.newArrayList(
+                topologyService.getPaths(topologyService.currentTopology(), 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();
+            Set<PortNumber> existingPorts = getPorts(nextObj.next());
+            for (Path path : allPaths) {
+                PortNumber srcPort = path.links().get(0).src().port();
+                if (existingPorts.contains(srcPort)) {
+                    return Optional.of(path);
+                }
+            }
+        }
+        // Otherwise, randomly pick a path
+        Collections.shuffle(allPaths);
+        return allPaths.stream().findFirst();
+    }
+
+    /**
+     * Gets egress VLAN from McastConfig.
+     *
+     * @return egress VLAN or VlanId.NONE if not configured
+     */
+    private VlanId egressVlan() {
+        McastConfig mcastConfig =
+                srManager.cfgService.getConfig(coreAppId, McastConfig.class);
+        return (mcastConfig != null) ? mcastConfig.egressVlan() : VlanId.NONE;
+    }
+
+    /**
+     * Gets assigned VLAN according to the value of egress VLAN.
+     *
+     * @return assigned VLAN
+     */
+    private VlanId assignedVlan() {
+        return (egressVlan().equals(VlanId.NONE)) ?
+                VlanId.vlanId(SegmentRoutingManager.ASSIGNED_VLAN_NO_SUBNET) :
+                egressVlan();
+    }
+}
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/NetworkConfigEventHandler.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/NetworkConfigEventHandler.java
index 7e9dd55..0c3fac7 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/NetworkConfigEventHandler.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/NetworkConfigEventHandler.java
@@ -73,8 +73,8 @@
         SegmentRoutingAppConfig config = (SegmentRoutingAppConfig) event.config().get();
         SegmentRoutingAppConfig prevConfig = (SegmentRoutingAppConfig) event.prevConfig().get();
         deviceService.getAvailableDevices().forEach(device -> {
-            Set<MacAddress> macAddresses = getMacAddresses(config);
-            Set<MacAddress> prevMacAddresses = getMacAddresses(prevConfig);
+            Set<MacAddress> macAddresses = new HashSet<>(getMacAddresses(config));
+            Set<MacAddress> prevMacAddresses = new HashSet<>(getMacAddresses(prevConfig));
             // Avoid removing and re-adding unchanged MAC addresses since
             // FlowObjective does not guarantee the execution order.
             Set<MacAddress> sameMacAddresses = new HashSet<>(macAddresses);
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 9716b52..7a11740 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -33,6 +33,7 @@
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
 import org.onosproject.event.Event;
+import org.onosproject.incubator.net.config.basics.McastConfig;
 import org.onosproject.mastership.MastershipService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.Device;
@@ -60,23 +61,27 @@
 import org.onosproject.net.flowobjective.ObjectiveError;
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.net.host.HostListener;
+import org.onosproject.net.mcast.McastEvent;
+import org.onosproject.net.mcast.McastListener;
+import org.onosproject.net.mcast.MulticastRouteService;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.topology.TopologyService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.NeighborSet;
+import org.onosproject.segmentrouting.grouphandler.NeighborSetNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.grouphandler.PortNextObjectiveStoreKey;
 import org.onosproject.net.host.HostService;
 import org.onosproject.net.link.LinkEvent;
 import org.onosproject.net.link.LinkListener;
 import org.onosproject.net.link.LinkService;
 import org.onosproject.net.packet.InboundPacket;
 import org.onosproject.net.packet.PacketContext;
-import org.onosproject.net.packet.PacketPriority;
 import org.onosproject.net.packet.PacketProcessor;
 import org.onosproject.net.packet.PacketService;
-import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
-import org.onosproject.segmentrouting.config.DeviceConfiguration;
-import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
-import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
-import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
-import org.onosproject.segmentrouting.grouphandler.NeighborSet;
-import org.onosproject.segmentrouting.grouphandler.NeighborSetNextObjectiveStoreKey;
-import org.onosproject.segmentrouting.grouphandler.PortNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.grouphandler.SubnetNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.grouphandler.XConnectNextObjectiveStoreKey;
 import org.onosproject.store.serializers.KryoNamespaces;
@@ -143,6 +148,12 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected ComponentConfigService compCfgService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MulticastRouteService multicastRouteService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected TopologyService topologyService;
+
     protected ArpHandler arpHandler = null;
     protected IcmpHandler icmpHandler = null;
     protected IpHandler ipHandler = null;
@@ -157,8 +168,11 @@
     private InternalLinkListener linkListener = null;
     private InternalDeviceListener deviceListener = null;
     private NetworkConfigEventHandler netcfgHandler = null;
+    private McastEventHandler mcastEventHandler = null;
     private InternalEventHandler eventHandler = new InternalEventHandler();
     private final InternalHostListener hostListener = new InternalHostListener();
+    private final InternalConfigListener cfgListener = new InternalConfigListener(this);
+    private final InternalMcastListener mcastListener = new InternalMcastListener();
 
     private ScheduledExecutorService executorService = Executors
             .newScheduledThreadPool(1);
@@ -196,29 +210,32 @@
     private EventuallyConsistentMap<String, Tunnel> tunnelStore = null;
     private EventuallyConsistentMap<String, Policy> policyStore = null;
 
-    private final InternalConfigListener cfgListener =
-            new InternalConfigListener(this);
-
-    private final ConfigFactory<DeviceId, SegmentRoutingDeviceConfig> cfgDeviceFactory =
+    private final ConfigFactory<DeviceId, SegmentRoutingDeviceConfig> deviceConfigFactory =
             new ConfigFactory<DeviceId, SegmentRoutingDeviceConfig>(SubjectFactories.DEVICE_SUBJECT_FACTORY,
-                              SegmentRoutingDeviceConfig.class,
-                              "segmentrouting") {
+                    SegmentRoutingDeviceConfig.class, "segmentrouting") {
                 @Override
                 public SegmentRoutingDeviceConfig createConfig() {
                     return new SegmentRoutingDeviceConfig();
                 }
             };
-
-    private final ConfigFactory<ApplicationId, SegmentRoutingAppConfig> cfgAppFactory =
+    private final ConfigFactory<ApplicationId, SegmentRoutingAppConfig> appConfigFactory =
             new ConfigFactory<ApplicationId, SegmentRoutingAppConfig>(SubjectFactories.APP_SUBJECT_FACTORY,
-                    SegmentRoutingAppConfig.class,
-                    "segmentrouting") {
+                    SegmentRoutingAppConfig.class, "segmentrouting") {
                 @Override
                 public SegmentRoutingAppConfig createConfig() {
                     return new SegmentRoutingAppConfig();
                 }
             };
 
+    private ConfigFactory<ApplicationId, McastConfig> mcastConfigFactory =
+            new ConfigFactory<ApplicationId, McastConfig>(SubjectFactories.APP_SUBJECT_FACTORY,
+                    McastConfig.class, "multicast") {
+                @Override
+                public McastConfig createConfig() {
+                    return new McastConfig();
+                }
+            };
+
     private Object threadSchedulerLock = new Object();
     private static int numOfEventsQueued = 0;
     private static int numOfEventsExecuted = 0;
@@ -312,14 +329,17 @@
         linkListener = new InternalLinkListener();
         deviceListener = new InternalDeviceListener();
         netcfgHandler = new NetworkConfigEventHandler(this);
+        mcastEventHandler = new McastEventHandler(this);
 
         cfgService.addListener(cfgListener);
-        cfgService.registerConfigFactory(cfgDeviceFactory);
-        cfgService.registerConfigFactory(cfgAppFactory);
+        cfgService.registerConfigFactory(deviceConfigFactory);
+        cfgService.registerConfigFactory(appConfigFactory);
+        cfgService.registerConfigFactory(mcastConfigFactory);
         hostService.addListener(hostListener);
         packetService.addProcessor(processor, PacketProcessor.director(2));
         linkService.addListener(linkListener);
         deviceService.addListener(deviceListener);
+        multicastRouteService.addListener(mcastListener);
 
         // Request ARP packet-in
         TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
@@ -351,8 +371,9 @@
     @Deactivate
     protected void deactivate() {
         cfgService.removeListener(cfgListener);
-        cfgService.unregisterConfigFactory(cfgDeviceFactory);
-        cfgService.unregisterConfigFactory(cfgAppFactory);
+        cfgService.unregisterConfigFactory(deviceConfigFactory);
+        cfgService.unregisterConfigFactory(appConfigFactory);
+        cfgService.unregisterConfigFactory(mcastConfigFactory);
 
         // Withdraw ARP packet-in
         TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
@@ -362,12 +383,20 @@
         packetService.removeProcessor(processor);
         linkService.removeListener(linkListener);
         deviceService.removeListener(deviceListener);
+        multicastRouteService.removeListener(mcastListener);
+
         processor = null;
         linkListener = null;
-        deviceService = null;
-
+        deviceListener = null;
         groupHandlerMap.clear();
 
+        nsNextObjStore.destroy();
+        subnetNextObjStore.destroy();
+        portNextObjStore.destroy();
+        xConnectNextObjStore.destroy();
+        tunnelStore.destroy();
+        policyStore.destroy();
+        subnetVidStore.destroy();
         log.info("Stopped");
     }
 
@@ -1186,6 +1215,27 @@
         }
     }
 
+    private class InternalMcastListener implements McastListener {
+        @Override
+        public void event(McastEvent event) {
+            switch (event.type()) {
+                case SOURCE_ADDED:
+                    mcastEventHandler.processSourceAdded(event);
+                    break;
+                case SINK_ADDED:
+                    mcastEventHandler.processSinkAdded(event);
+                    break;
+                case SINK_REMOVED:
+                    mcastEventHandler.processSinkRemoved(event);
+                    break;
+                case ROUTE_ADDED:
+                case ROUTE_REMOVED:
+                default:
+                    break;
+            }
+        }
+    }
+
     private static class BridgingTableObjectiveContext implements ObjectiveContext {
         final MacAddress mac;
         final VlanId vlanId;
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
index 9544619..c481098 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
@@ -496,12 +496,22 @@
         return srinfo != null && srinfo.adjacencySids.containsKey(sid);
     }
 
+    /**
+     * Gets connect points for which segment routing does not install subnet rules.
+     *
+     * @return set of connect points
+     */
     public Set<ConnectPoint> suppressSubnet() {
         SegmentRoutingAppConfig appConfig =
                 cfgService.getConfig(appId, SegmentRoutingAppConfig.class);
         return (appConfig != null) ? appConfig.suppressSubnet() : ImmutableSet.of();
     }
 
+    /**
+     * Gets connect points for which segment routing does not install host rules.
+     *
+     * @return set of connect points
+     */
     public Set<ConnectPoint> suppressHost() {
         SegmentRoutingAppConfig appConfig =
                 cfgService.getConfig(appId, SegmentRoutingAppConfig.class);
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/grouphandler/McastNextObjectiveStoreKey.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/grouphandler/McastNextObjectiveStoreKey.java
new file mode 100644
index 0000000..526db72
--- /dev/null
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/grouphandler/McastNextObjectiveStoreKey.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.DeviceId;
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import java.util.Objects;
+
+/**
+ * Key of multicast next objective store.
+ */
+public class McastNextObjectiveStoreKey {
+    private final IpAddress mcastIp;
+    private final DeviceId deviceId;
+
+    /**
+     * Constructs the key of multicast next objective store.
+     *
+     * @param mcastIp multicast group IP address
+     * @param deviceId device ID
+     */
+    public McastNextObjectiveStoreKey(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");
+        this.mcastIp = mcastIp;
+        this.deviceId = deviceId;
+    }
+
+    /**
+     * Returns the multicast IP address of this key.
+     *
+     * @return multicast IP
+     */
+    public IpAddress mcastIp() {
+        return this.mcastIp;
+    }
+
+    /**
+     * Returns the device ID of this key.
+     *
+     * @return device ID
+     */
+    public DeviceId deviceId() {
+        return this.deviceId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof McastNextObjectiveStoreKey)) {
+            return false;
+        }
+        McastNextObjectiveStoreKey that =
+                (McastNextObjectiveStoreKey) o;
+        return (Objects.equals(this.mcastIp, that.mcastIp) &&
+                Objects.equals(this.deviceId, that.deviceId));
+    }
+
+    @Override
+    public int hashCode() {
+         return Objects.hash(mcastIp, deviceId);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(getClass())
+                .add("mcastIp", mcastIp)
+                .add("deviceId", deviceId)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/mcast/McastRouteInfo.java b/core/api/src/main/java/org/onosproject/net/mcast/McastRouteInfo.java
index 31e51a0..f8d232b 100644
--- a/core/api/src/main/java/org/onosproject/net/mcast/McastRouteInfo.java
+++ b/core/api/src/main/java/org/onosproject/net/mcast/McastRouteInfo.java
@@ -22,6 +22,7 @@
 import java.util.Optional;
 import java.util.Set;
 
+import static com.google.common.base.MoreObjects.toStringHelper;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
@@ -102,4 +103,13 @@
         return sinks;
     }
 
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("route", route())
+                .add("sink", sink())
+                .add("source", source())
+                .add("sinks", sinks())
+                .toString();
+    }
 }
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/CpqdOfdpa2Pipeline.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/CpqdOfdpa2Pipeline.java
index 6bcebd7..0b945a3 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/CpqdOfdpa2Pipeline.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/CpqdOfdpa2Pipeline.java
@@ -22,8 +22,6 @@
 import java.util.Collections;
 import java.util.Deque;
 import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -279,17 +277,6 @@
         }
 
         for (PortNumber pnum : portnums) {
-            // update storage
-            groupHandler.port2Vlan.put(pnum, storeVlan);
-            Set<PortNumber> vlanPorts = groupHandler.vlan2Port.get(storeVlan);
-            if (vlanPorts == null) {
-                vlanPorts = Collections.newSetFromMap(
-                                    new ConcurrentHashMap<PortNumber, Boolean>());
-                vlanPorts.add(pnum);
-                groupHandler.vlan2Port.put(storeVlan, vlanPorts);
-            } else {
-                vlanPorts.add(pnum);
-            }
             // create rest of flowrule
             selector.matchInPort(pnum);
             FlowRule rule = DefaultFlowRule.builder()
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java
index 9c3c9eb..a9c2bb1 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2GroupHandler.java
@@ -65,7 +65,6 @@
 import java.util.Collections;
 import java.util.Deque;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -123,10 +122,6 @@
     // index number for group creation
     private AtomicCounter nextIndex;
 
-    // local stores for port-vlan mapping
-    protected Map<PortNumber, VlanId> port2Vlan = new ConcurrentHashMap<>();
-    protected Map<VlanId, Set<PortNumber>> vlan2Port = new ConcurrentHashMap<>();
-
     // local store for pending bucketAdds - by design there can only be one
     // pending bucket for a group
     protected ConcurrentHashMap<Integer, NextObjective> pendingBuckets = new ConcurrentHashMap<>();
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2Pipeline.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2Pipeline.java
index be35026..97751ec 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2Pipeline.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/Ofdpa2Pipeline.java
@@ -313,31 +313,22 @@
         }
 
         VlanId assignedVlan = null;
-        // For VLAN cross-connect packets, use the configured VLAN
         if (vidCriterion != null) {
-            if (vidCriterion.vlanId() != VlanId.NONE) {
+            // Use the VLAN in metadata whenever a metadata is provided
+            if (filt.meta() != null) {
+                assignedVlan = readVlanFromTreatment(filt.meta());
+            // Use the VLAN in criterion if metadata is not present and the traffic is tagged
+            } else if (!vidCriterion.vlanId().equals(VlanId.NONE)) {
                 assignedVlan = vidCriterion.vlanId();
+            }
 
-            // For untagged packets, assign a VLAN ID
-            } else {
-                if (filt.meta() == null) {
-                    log.error("Missing metadata in filtering objective required " +
-                            "for vlan assignment in dev {}", deviceId);
-                    fail(filt, ObjectiveError.BADPARAMS);
-                    return;
-                }
-                for (Instruction i : filt.meta().allInstructions()) {
-                    if (i instanceof ModVlanIdInstruction) {
-                        assignedVlan = ((ModVlanIdInstruction) i).vlanId();
-                    }
-                }
-                if (assignedVlan == null) {
-                    log.error("Driver requires an assigned vlan-id to tag incoming "
-                            + "untagged packets. Not processing vlan filters on "
-                            + "device {}", deviceId);
-                    fail(filt, ObjectiveError.BADPARAMS);
-                    return;
-                }
+            if (assignedVlan == null) {
+                log.error("Driver fails to extract VLAN information. "
+                        + "Not proccessing VLAN filters on device {}.", deviceId);
+                log.debug("VLAN ID in criterion={}, metadata={}",
+                        readVlanFromTreatment(filt.meta()), vidCriterion.vlanId());
+                fail(filt, ObjectiveError.BADPARAMS);
+                return;
             }
         }
 
@@ -457,22 +448,14 @@
         TrafficSelector.Builder preSelector = null;
         TrafficTreatment.Builder preTreatment = null;
 
-
         treatment.transition(TMAC_TABLE);
 
-        VlanId storeVlan = null;
         if (vidCriterion.vlanId() == VlanId.NONE) {
             // untagged packets are assigned vlans
             OfdpaMatchVlanVid ofdpaMatchVlanVid = new OfdpaMatchVlanVid(VlanId.NONE);
             selector.extension(ofdpaMatchVlanVid, deviceId);
             OfdpaSetVlanVid ofdpaSetVlanVid = new OfdpaSetVlanVid(assignedVlan);
             treatment.extension(ofdpaSetVlanVid, deviceId);
-            // ofdpa requires an additional vlan match rule for the assigned vlan
-            // and it does not require the push when setting the assigned vlan.
-            // It also requires the extra rule to be sent to the switch before we
-            // send the untagged match rule.
-            // None of this in compliance with OF standard.
-            storeVlan = assignedVlan;
 
             preSelector = DefaultTrafficSelector.builder();
             OfdpaMatchVlanVid preOfdpaMatchVlanVid = new OfdpaMatchVlanVid(assignedVlan);
@@ -482,7 +465,11 @@
         } else {
             OfdpaMatchVlanVid ofdpaMatchVlanVid = new OfdpaMatchVlanVid(vidCriterion.vlanId());
             selector.extension(ofdpaMatchVlanVid, deviceId);
-            storeVlan = vidCriterion.vlanId();
+
+            if (!assignedVlan.equals(vidCriterion.vlanId())) {
+                OfdpaSetVlanVid ofdpaSetVlanVid = new OfdpaSetVlanVid(assignedVlan);
+                treatment.extension(ofdpaSetVlanVid, deviceId);
+            }
         }
 
         // ofdpa cannot match on ALL portnumber, so we need to use separate
@@ -499,17 +486,6 @@
         }
 
         for (PortNumber pnum : portnums) {
-            // update storage
-            groupHandler.port2Vlan.put(pnum, storeVlan);
-            Set<PortNumber> vlanPorts = groupHandler.vlan2Port.get(storeVlan);
-            if (vlanPorts == null) {
-                vlanPorts = Collections.newSetFromMap(
-                                    new ConcurrentHashMap<PortNumber, Boolean>());
-                vlanPorts.add(pnum);
-                groupHandler.vlan2Port.put(storeVlan, vlanPorts);
-            } else {
-                vlanPorts.add(pnum);
-            }
             // create rest of flowrule
             selector.matchInPort(pnum);
             FlowRule rule = DefaultFlowRule.builder()
@@ -1112,4 +1088,13 @@
         Criterion criterion = selector.getCriterion(Criterion.Type.IPV4_DST);
         return (criterion == null) ? null : ((IPCriterion) criterion).ip();
     }
+
+    private static VlanId readVlanFromTreatment(TrafficTreatment treatment) {
+        for (Instruction i : treatment.allInstructions()) {
+            if (i instanceof ModVlanIdInstruction) {
+                return ((ModVlanIdInstruction) i).vlanId();
+            }
+        }
+        return null;
+    }
 }
diff --git a/incubator/api/src/main/java/org/onosproject/incubator/net/config/basics/McastConfig.java b/incubator/api/src/main/java/org/onosproject/incubator/net/config/basics/McastConfig.java
new file mode 100644
index 0000000..dd21ed6
--- /dev/null
+++ b/incubator/api/src/main/java/org/onosproject/incubator/net/config/basics/McastConfig.java
@@ -0,0 +1,101 @@
+/*
+ * 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.incubator.net.config.basics;
+
+import com.google.common.annotations.Beta;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.config.Config;
+
+/**
+ * Configuration for multicast.
+ */
+@Beta
+public class McastConfig extends Config<ApplicationId> {
+    private static final String INGRESS_VLAN = "ingressVlan";
+    private static final String EGRESS_VLAN = "egressVlan";
+
+    @Override
+    public boolean isValid() {
+        return hasOnlyFields(INGRESS_VLAN, EGRESS_VLAN) &&
+                ingressVlan() != null && egressVlan() != null;
+    }
+
+    /**
+     * Gets ingress VLAN of multicast traffic.
+     *
+     * @return Ingress VLAN ID
+     */
+    public VlanId ingressVlan() {
+        if (!object.has(INGRESS_VLAN)) {
+            return VlanId.NONE;
+        }
+
+        try {
+            return VlanId.vlanId(object.path(INGRESS_VLAN).asText());
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Sets ingress VLAN of multicast traffic.
+     *
+     * @param vlanId Ingress VLAN ID
+     * @return this {@link McastConfig}
+     */
+    public McastConfig setIngressVlan(VlanId vlanId) {
+        if (vlanId == null) {
+            object.remove(INGRESS_VLAN);
+        } else {
+            object.put(INGRESS_VLAN, vlanId.toString());
+        }
+        return this;
+    }
+
+    /**
+     * Gets egress VLAN of multicast traffic.
+     *
+     * @return Egress VLAN ID
+     */
+    public VlanId egressVlan() {
+        if (!object.has(EGRESS_VLAN)) {
+            return VlanId.NONE;
+        }
+
+        try {
+            return VlanId.vlanId(object.path(EGRESS_VLAN).asText());
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Sets egress VLAN of multicast traffic.
+     *
+     * @param vlanId Egress VLAN ID
+     * @return this {@link McastConfig}
+     */
+    public McastConfig setEgressVlan(VlanId vlanId) {
+        if (vlanId == null) {
+            object.remove(EGRESS_VLAN);
+        } else {
+            object.put(EGRESS_VLAN, vlanId.toString());
+        }
+        return this;
+    }
+}
diff --git a/incubator/api/src/test/java/org/onosproject/incubator/net/config/basics/McastConfigTest.java b/incubator/api/src/test/java/org/onosproject/incubator/net/config/basics/McastConfigTest.java
new file mode 100644
index 0000000..f78709d
--- /dev/null
+++ b/incubator/api/src/test/java/org/onosproject/incubator/net/config/basics/McastConfigTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.incubator.net.config.basics;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.annotations.Beta;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.VlanId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+
+import java.io.InputStream;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for class {@link McastConfig}.
+ */
+@Beta
+public class McastConfigTest {
+    private static final TestApplicationId APP_ID =
+            new TestApplicationId(CoreService.CORE_APP_NAME);
+    private McastConfig config;
+    private McastConfig invalidConfig;
+
+    private static final VlanId INGRESS_VLAN_1 = VlanId.NONE;
+    private static final VlanId EGRESS_VLAN_1 = VlanId.NONE;
+    private static final VlanId INGRESS_VLAN_2 = VlanId.vlanId((short) 100);
+    private static final VlanId EGRESS_VLAN_2 = VlanId.vlanId((short) 100);
+
+    /**
+     * Initialize test related variables.
+     *
+     * @throws Exception
+     */
+    @Before
+    public void setUp() throws Exception {
+        InputStream jsonStream = McastConfigTest.class
+                .getResourceAsStream("/mcast-config.json");
+        InputStream invalidJsonStream = McastConfigTest.class
+                .getResourceAsStream("/mcast-config-invalid.json");
+
+        ApplicationId subject = APP_ID;
+        String key = CoreService.CORE_APP_NAME;
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode jsonNode = mapper.readTree(jsonStream);
+        JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+        ConfigApplyDelegate delegate = new MockDelegate();
+
+        config = new McastConfig();
+        config.init(subject, key, jsonNode, mapper, delegate);
+        invalidConfig = new McastConfig();
+        invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+    }
+
+    /**
+     * Tests config validity.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void isValid() throws Exception {
+        assertTrue(config.isValid());
+        assertFalse(invalidConfig.isValid());
+    }
+
+    /**
+     * Tests ingress VLAN getter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void ingressVlan() throws Exception {
+        VlanId ingressVlan = config.ingressVlan();
+        assertNotNull("ingressVlan should not be null", ingressVlan);
+        assertThat(ingressVlan, is(INGRESS_VLAN_1));
+    }
+
+    /**
+     * Tests ingress VLAN setter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void setIngressVlan() throws Exception {
+        config.setIngressVlan(INGRESS_VLAN_2);
+
+        VlanId ingressVlan = config.ingressVlan();
+        assertNotNull("ingressVlan should not be null", ingressVlan);
+        assertThat(ingressVlan, is(INGRESS_VLAN_2));
+    }
+
+    /**
+     * Tests egress VLAN getter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void egressVlan() throws Exception {
+        VlanId egressVlan = config.egressVlan();
+        assertNotNull("egressVlan should not be null", egressVlan);
+        assertThat(egressVlan, is(EGRESS_VLAN_1));
+    }
+
+    /**
+     * Tests egress VLAN setter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void setEgressVlan() throws Exception {
+        config.setEgressVlan(EGRESS_VLAN_2);
+
+        VlanId egressVlan = config.egressVlan();
+        assertNotNull("egressVlan should not be null", egressVlan);
+        assertThat(egressVlan, is(EGRESS_VLAN_2));
+    }
+
+    private class MockDelegate implements ConfigApplyDelegate {
+        @Override
+        public void onApply(Config config) {
+        }
+    }
+}
\ No newline at end of file
diff --git a/incubator/api/src/test/resources/mcast-config-invalid.json b/incubator/api/src/test/resources/mcast-config-invalid.json
new file mode 100644
index 0000000..3d7c3a0
--- /dev/null
+++ b/incubator/api/src/test/resources/mcast-config-invalid.json
@@ -0,0 +1,4 @@
+{
+  "ingressVlan" : "None",
+  "egressVlan" : "5000"
+}
\ No newline at end of file
diff --git a/incubator/api/src/test/resources/mcast-config.json b/incubator/api/src/test/resources/mcast-config.json
new file mode 100644
index 0000000..ba8eaeb
--- /dev/null
+++ b/incubator/api/src/test/resources/mcast-config.json
@@ -0,0 +1,4 @@
+{
+  "ingressVlan" : "None",
+  "egressVlan" : "None"
+}
\ No newline at end of file