Prevent XConnect loop

Change-Id: I65c52342840ebef944a65b8c6f65a33448da59cf
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index f083d74..a41c58d 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -997,6 +997,11 @@
     }
 
     @Override
+    public boolean shouldProgram(DeviceId deviceId) {
+        return defaultRoutingHandler.shouldProgram(deviceId);
+    }
+
+    @Override
     public ApplicationId appId() {
         return appId;
     }
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
index a29b784..1b28998 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
@@ -353,6 +353,14 @@
     Map<DeviceId, Boolean> getShouldProgramCache();
 
     /**
+     * Returns whether instance should program device or not.
+     *
+     * @param deviceId .
+     * @return boolean status saying instance should program device or not.
+     */
+    boolean shouldProgram(DeviceId deviceId);
+
+    /**
      * Gets application id.
      *
      * @return application id
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
index 25f1b01..4e82524 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
@@ -22,6 +22,7 @@
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.PortNumber;
 
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -73,6 +74,24 @@
     boolean hasXconnect(ConnectPoint cp);
 
     /**
+     * Gives xconnect VLAN of given port of a device.
+     *
+     * @param deviceId Device ID
+     * @param port Port number
+     * @return true if given VLAN vlanId is XConnect VLAN on device deviceId.
+     */
+    List<VlanId> getXconnectVlans(DeviceId deviceId, PortNumber port);
+
+    /**
+     * Checks given VLAN is XConnect VLAN in given device.
+     *
+     * @param deviceId Device ID
+     * @param vlanId VLAN ID
+     * @return true if given VLAN vlanId is XConnect VLAN on device deviceId.
+     */
+    boolean isXconnectVlan(DeviceId deviceId, VlanId vlanId);
+
+    /**
      * Returns the Xconnect next objective store.
      *
      * @return current contents of the xconnectNextObjStore
@@ -86,4 +105,12 @@
      */
     void removeNextId(int nextId);
 
+    /**
+     * Returns Xconnect next objective ID associated with group device + vlan.
+     *
+     * @param deviceId - Device ID
+     * @param vlanId - VLAN ID
+     * @return Current associated group ID
+     */
+    int getNextId(DeviceId deviceId, VlanId vlanId);
 }
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
index a7eb664..b65faa9 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
@@ -24,6 +24,7 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.Ethernet;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 import org.onlab.util.KryoNamespace;
@@ -33,6 +34,8 @@
 import org.onosproject.mastership.MastershipService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostLocation;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.config.NetworkConfigService;
 import org.onosproject.net.device.DeviceEvent;
@@ -54,7 +57,12 @@
 import org.onosproject.net.flowobjective.Objective;
 import org.onosproject.net.flowobjective.ObjectiveContext;
 import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intf.InterfaceService;
 import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.xconnect.api.XconnectCodec;
 import org.onosproject.segmentrouting.xconnect.api.XconnectDesc;
 import org.onosproject.segmentrouting.xconnect.api.XconnectKey;
@@ -70,6 +78,11 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
@@ -107,6 +120,12 @@
     @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
     public SegmentRoutingService srService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    HostService hostService;
+
     private static final String APP_NAME = "org.onosproject.xconnect";
     private static final String ERROR_NOT_MASTER = "Not master controller";
 
@@ -116,11 +135,18 @@
     private ConsistentMap<XconnectKey, Set<PortNumber>> xconnectStore;
     private ConsistentMap<XconnectKey, Integer> xconnectNextObjStore;
 
+    private ConsistentMap<VlanNextObjectiveStoreKey, Integer> xconnectMulticastNextStore;
+    private ConsistentMap<VlanNextObjectiveStoreKey, List<PortNumber>> xconnectMulticastPortsStore;
+
     private final MapEventListener<XconnectKey, Set<PortNumber>> xconnectListener = new XconnectMapListener();
     private final DeviceListener deviceListener = new InternalDeviceListener();
 
     private ExecutorService deviceEventExecutor;
 
+    private final HostListener hostListener = new InternalHostListener();
+    private ExecutorService hostEventExecutor;
+
+
     @Activate
     void activate() {
         appId = coreService.registerApplication(APP_NAME);
@@ -129,7 +155,8 @@
         KryoNamespace.Builder serializer = KryoNamespace.newBuilder()
                 .register(KryoNamespaces.API)
                 .register(XconnectManager.class)
-                .register(XconnectKey.class);
+                .register(XconnectKey.class)
+                .register(VlanNextObjectiveStoreKey.class);
 
         xconnectStore = storageService.<XconnectKey, Set<PortNumber>>consistentMapBuilder()
                 .withName("onos-sr-xconnect")
@@ -144,11 +171,25 @@
                 .withSerializer(Serializer.using(serializer.build()))
                 .build();
 
+        xconnectMulticastNextStore = storageService.<VlanNextObjectiveStoreKey, Integer>consistentMapBuilder()
+                .withName("onos-sr-xconnect-l2multicast-next")
+                .withSerializer(Serializer.using(serializer.build()))
+                .build();
+        xconnectMulticastPortsStore = storageService.<VlanNextObjectiveStoreKey, List<PortNumber>>consistentMapBuilder()
+                .withName("onos-sr-xconnect-l2multicast-ports")
+                .withSerializer(Serializer.using(serializer.build()))
+                .build();
+
         deviceEventExecutor = Executors.newSingleThreadScheduledExecutor(
                 groupedThreads("sr-xconnect-device-event", "%d", log));
 
         deviceService.addListener(deviceListener);
 
+        hostEventExecutor = Executors.newSingleThreadExecutor(
+                groupedThreads("sr-xconnect-host-event", "%d", log));
+
+        hostService.addListener(hostListener);
+
         log.info("Started");
     }
 
@@ -156,9 +197,11 @@
     void deactivate() {
         xconnectStore.removeListener(xconnectListener);
         deviceService.removeListener(deviceListener);
+        hostService.removeListener(hostListener);
         codecService.unregisterCodec(XconnectDesc.class);
 
         deviceEventExecutor.shutdown();
+        hostEventExecutor.shutdown();
 
         log.info("Stopped");
     }
@@ -166,7 +209,7 @@
     @Override
     public void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<PortNumber> ports) {
         log.info("Adding or updating xconnect. deviceId={}, vlanId={}, ports={}",
-                deviceId, vlanId, ports);
+                 deviceId, vlanId, ports);
         final XconnectKey key = new XconnectKey(deviceId, vlanId);
         xconnectStore.put(key, ports);
     }
@@ -174,9 +217,15 @@
     @Override
     public void removeXonnect(DeviceId deviceId, VlanId vlanId) {
         log.info("Removing xconnect. deviceId={}, vlanId={}",
-                deviceId, vlanId);
+                 deviceId, vlanId);
         final XconnectKey key = new XconnectKey(deviceId, vlanId);
         xconnectStore.remove(key);
+
+        // Cleanup multicasting support, if any.
+        srService.getPairDeviceId(deviceId).ifPresent(pairDeviceId -> {
+            cleanupL2MulticastRule(pairDeviceId, srService.getPairLocalPort(pairDeviceId).get(), vlanId, true);
+        });
+
     }
 
     @Override
@@ -189,11 +238,27 @@
     @Override
     public boolean hasXconnect(ConnectPoint cp) {
         return getXconnects().stream().anyMatch(desc ->
-                desc.key().deviceId().equals(cp.deviceId()) && desc.ports().contains(cp.port())
+                                                        desc.key().deviceId().equals(cp.deviceId())
+                                                                && desc.ports().contains(cp.port())
         );
     }
 
     @Override
+    public List<VlanId> getXconnectVlans(DeviceId deviceId, PortNumber port) {
+        return getXconnects().stream()
+                .filter(desc -> desc.key().deviceId().equals(deviceId) && desc.ports().contains(port))
+                .map(XconnectDesc::key)
+                .map(XconnectKey::vlanId)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean isXconnectVlan(DeviceId deviceId, VlanId vlanId) {
+        return getXconnects().stream()
+                .anyMatch(desc -> desc.key().deviceId().equals(deviceId) && desc.key().vlanId().equals(vlanId));
+    }
+
+    @Override
     public ImmutableMap<XconnectKey, Integer> getNext() {
         if (xconnectNextObjStore != null) {
             return ImmutableMap.copyOf(xconnectNextObjStore.asJavaMap());
@@ -203,6 +268,15 @@
     }
 
     @Override
+    public int getNextId(final DeviceId deviceId, final VlanId vlanId) {
+        Optional<Integer> nextObjective = getNext().entrySet().stream()
+                .filter(d -> d.getKey().deviceId().equals(deviceId) && d.getKey().vlanId().equals(vlanId))
+                .findFirst()
+                .map(Map.Entry::getValue);
+        return nextObjective.isPresent() ? nextObjective.get() : -1;
+    }
+
+    @Override
     public void removeNextId(int nextId) {
         xconnectNextObjStore.entrySet().forEach(e -> {
             if (e.getValue().value() == nextId) {
@@ -260,6 +334,98 @@
         }
     }
 
+    private class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            hostEventExecutor.execute(() -> {
+
+                switch (event.type()) {
+                    case HOST_MOVED:
+                        log.trace("Processing host event {}", event);
+
+                        Host host = event.subject();
+                        Set<HostLocation> prevLocations = event.prevSubject().locations();
+                        Set<HostLocation> newLocations = host.locations();
+
+                        // Dual-home host port failure
+                        // For each old location, in failed and paired devices update L2 vlan groups
+                        Sets.difference(prevLocations, newLocations).forEach(prevLocation -> {
+
+                            Optional<DeviceId> pairDeviceId = srService.getPairDeviceId(prevLocation.deviceId());
+                            Optional<PortNumber> pairLocalPort = srService.getPairLocalPort(prevLocation.deviceId());
+
+                            if (pairDeviceId.isPresent() && pairLocalPort.isPresent() && newLocations.stream()
+                                    .anyMatch(location -> location.deviceId().equals(pairDeviceId.get())) &&
+                                    hasXconnect(new ConnectPoint(prevLocation.deviceId(), prevLocation.port()))) {
+
+                                List<VlanId> xconnectVlans = getXconnectVlans(prevLocation.deviceId(),
+                                                                              prevLocation.port());
+                                xconnectVlans.forEach(xconnectVlan -> {
+                                    // Add single-home host into L2 multicast group at paired device side.
+                                    // Also append ACL rule to forward traffic from paired port to L2 multicast group.
+                                    newLocations.stream()
+                                            .filter(location -> location.deviceId().equals(pairDeviceId.get()))
+                                            .forEach(location -> populateL2Multicast(location.deviceId(),
+                                                                                     srService.getPairLocalPort(
+                                                                                             location.deviceId()).get(),
+                                                                                     xconnectVlan,
+                                                                                     Collections.singletonList(
+                                                                                             location.port())));
+                               // Ensure pair-port attached to xconnect vlan flooding group at dual home failed device.
+                                    updateL2Flooding(prevLocation.deviceId(), pairLocalPort.get(), xconnectVlan, true);
+                                });
+                            }
+                        });
+
+                        // Dual-home host port restoration
+                        // For each new location, reverse xconnect loop prevention groups.
+                        Sets.difference(newLocations, prevLocations).forEach(newLocation -> {
+                            final Optional<DeviceId> pairDeviceId = srService.getPairDeviceId(newLocation.deviceId());
+                            Optional<PortNumber> pairLocalPort = srService.getPairLocalPort(newLocation.deviceId());
+                            if (pairDeviceId.isPresent() && pairLocalPort.isPresent() &&
+                                    hasXconnect((new ConnectPoint(newLocation.deviceId(), newLocation.port())))) {
+
+                                List<VlanId> xconnectVlans = getXconnectVlans(newLocation.deviceId(),
+                                                                              newLocation.port());
+                                xconnectVlans.forEach(xconnectVlan -> {
+                                    // Remove recovered dual homed port from vlan L2 multicast group
+                                    prevLocations.stream()
+                                            .filter(prevLocation -> prevLocation.deviceId().equals(pairDeviceId.get()))
+                                            .forEach(prevLocation -> revokeL2Multicast(prevLocation.deviceId(),
+                                                                                 srService.getPairLocalPort(
+                                                                                       prevLocation.deviceId()).get(),
+                                                                                       xconnectVlan,
+                                                                      Collections.singletonList(newLocation.port()))
+                                            );
+
+                                 // Remove pair-port from vlan's flooding group at dual home restored device,if needed.
+                                    if (!hasAccessPortInMulticastGroup(newLocation.deviceId(),
+                                                                       xconnectVlan,
+                                                                       pairLocalPort.get())) {
+                                        updateL2Flooding(newLocation.deviceId(),
+                                                         pairLocalPort.get(),
+                                                         xconnectVlan,
+                                                         false);
+
+                                        // Clean L2 multicast group at pair-device; also update store.
+                                        cleanupL2MulticastRule(pairDeviceId.get(),
+                                                               srService.getPairLocalPort(pairDeviceId.get()).get(),
+                                                               xconnectVlan,
+                                                               false);
+                                    }
+                                });
+                            }
+                        });
+                        break;
+
+                    default:
+                        log.warn("Unsupported host event type: {} received. Ignoring.", event.type());
+                        break;
+                }
+            });
+        }
+    }
+
     private void init(DeviceId deviceId) {
         getXconnects().stream()
                 .filter(desc -> desc.key().deviceId().equals(deviceId))
@@ -276,7 +442,7 @@
     /**
      * Populates XConnect groups and flows for given key.
      *
-     * @param key XConnect key
+     * @param key   XConnect key
      * @param ports a set of ports to be cross-connected
      */
     private void populateXConnect(XconnectKey key, Set<PortNumber> ports) {
@@ -285,7 +451,6 @@
             return;
         }
 
-        ports = addPairPort(key.deviceId(), ports);
         populateFilter(key, ports);
         populateFwd(key, populateNext(key, ports));
         populateAcl(key);
@@ -294,7 +459,7 @@
     /**
      * Populates filtering objectives for given XConnect.
      *
-     * @param key XConnect store key
+     * @param key   XConnect store key
      * @param ports XConnect ports
      */
     private void populateFilter(XconnectKey key, Set<PortNumber> ports) {
@@ -302,10 +467,10 @@
             FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
             ObjectiveContext context = new DefaultObjectiveContext(
                     (objective) -> log.debug("XConnect FilterObj for {} on port {} populated",
-                            key, port),
+                                             key, port),
                     (objective, error) ->
                             log.warn("Failed to populate XConnect FilterObj for {} on port {}: {}",
-                                    key, port, error));
+                                     key, port, error));
             flowObjectiveService.filter(key.deviceId(), filtObjBuilder.add(context));
         });
     }
@@ -313,7 +478,7 @@
     /**
      * Populates next objectives for given XConnect.
      *
-     * @param key XConnect store key
+     * @param key   XConnect store key
      * @param ports XConnect ports
      */
     private int populateNext(XconnectKey key, Set<PortNumber> ports) {
@@ -342,7 +507,7 @@
     /**
      * Populates bridging forwarding objectives for given XConnect.
      *
-     * @param key XConnect store key
+     * @param key    XConnect store key
      * @param nextId next objective id
      */
     private void populateFwd(XconnectKey key, int nextId) {
@@ -371,7 +536,7 @@
     /**
      * Revokes XConnect groups and flows for given key.
      *
-     * @param key XConnect key
+     * @param key   XConnect key
      * @param ports XConnect ports
      */
     private void revokeXConnect(XconnectKey key, Set<PortNumber> ports) {
@@ -380,7 +545,6 @@
             return;
         }
 
-        ports = addPairPort(key.deviceId(), ports);
         revokeFilter(key, ports);
         if (xconnectNextObjStore.containsKey(key)) {
             int nextId = xconnectNextObjStore.get(key).value();
@@ -395,7 +559,7 @@
     /**
      * Revokes filtering objectives for given XConnect.
      *
-     * @param key XConnect store key
+     * @param key   XConnect store key
      * @param ports XConnect ports
      */
     private void revokeFilter(XconnectKey key, Set<PortNumber> ports) {
@@ -403,10 +567,10 @@
             FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
             ObjectiveContext context = new DefaultObjectiveContext(
                     (objective) -> log.debug("XConnect FilterObj for {} on port {} revoked",
-                            key, port),
+                                             key, port),
                     (objective, error) ->
                             log.warn("Failed to revoke XConnect FilterObj for {} on port {}: {}",
-                                    key, port, error));
+                                     key, port, error));
             flowObjectiveService.filter(key.deviceId(), filtObjBuilder.remove(context));
         });
     }
@@ -414,9 +578,9 @@
     /**
      * Revokes next objectives for given XConnect.
      *
-     * @param key XConnect store key
-     * @param ports ports in the XConnect
-     * @param nextId next objective id
+     * @param key        XConnect store key
+     * @param ports      ports in the XConnect
+     * @param nextId     next objective id
      * @param nextFuture completable future for this next objective operation
      */
     private void revokeNext(XconnectKey key, Set<PortNumber> ports, int nextId,
@@ -446,8 +610,8 @@
     /**
      * Revokes bridging forwarding objectives for given XConnect.
      *
-     * @param key XConnect store key
-     * @param nextId next objective id
+     * @param key       XConnect store key
+     * @param nextId    next objective id
      * @param fwdFuture completable future for this forwarding objective operation
      */
     private void revokeFwd(XconnectKey key, int nextId, CompletableFuture<ObjectiveError> fwdFuture) {
@@ -489,9 +653,9 @@
     /**
      * Updates XConnect groups and flows for given key.
      *
-     * @param key XConnect key
+     * @param key       XConnect key
      * @param prevPorts previous XConnect ports
-     * @param ports new XConnect ports
+     * @param ports     new XConnect ports
      */
     private void updateXConnect(XconnectKey key, Set<PortNumber> prevPorts,
                                 Set<PortNumber> ports) {
@@ -500,10 +664,12 @@
 
         // remove old filter
         prevPorts.stream().filter(port -> !ports.contains(port)).forEach(port ->
-                revokeFilter(key, ImmutableSet.of(port)));
+                                                                                 revokeFilter(key,
+                                                                                              ImmutableSet.of(port)));
         // install new filter
         ports.stream().filter(port -> !prevPorts.contains(port)).forEach(port ->
-                populateFilter(key, ImmutableSet.of(port)));
+                                                                                 populateFilter(key,
+                                                                                                ImmutableSet.of(port)));
 
         CompletableFuture<ObjectiveError> fwdFuture = new CompletableFuture<>();
         CompletableFuture<ObjectiveError> nextFuture = new CompletableFuture<>();
@@ -533,8 +699,8 @@
     /**
      * Creates a next objective builder for XConnect with given nextId.
      *
-     * @param key XConnect key
-     * @param ports set of XConnect ports
+     * @param key    XConnect key
+     * @param ports  set of XConnect ports
      * @param nextId next objective id
      * @return next objective builder
      */
@@ -556,7 +722,7 @@
     /**
      * Creates a next objective builder for XConnect.
      *
-     * @param key XConnect key
+     * @param key   XConnect key
      * @param ports set of XConnect ports
      * @return next objective builder
      */
@@ -569,7 +735,7 @@
     /**
      * Creates a bridging forwarding objective builder for XConnect.
      *
-     * @param key XConnect key
+     * @param key    XConnect key
      * @param nextId next ID of the broadcast group for this XConnect key
      * @return forwarding objective builder
      */
@@ -617,7 +783,7 @@
     /**
      * Creates a filtering objective builder for XConnect.
      *
-     * @param key XConnect key
+     * @param key  XConnect key
      * @param port XConnect ports
      * @return next objective builder
      */
@@ -631,18 +797,320 @@
     }
 
     /**
-     * Add pair port to the given set of port.
+     * Updates L2 flooding groups; add pair link into L2 flooding group of given xconnect vlan.
      *
-     * @param deviceId device Id
-     * @param ports ports specified in the xconnect config
-     * @return port specified in the xconnect config plus the pair port (if configured)
+     * @param deviceId Device ID
+     * @param port     Port details
+     * @param vlanId   VLAN ID
+     * @param install  Whether to add or revoke pair link addition to flooding group
      */
-    private Set<PortNumber> addPairPort(DeviceId deviceId, Set<PortNumber> ports) {
-        if (srService == null) {
-            return ports;
+    private void updateL2Flooding(DeviceId deviceId, final PortNumber port, VlanId vlanId, boolean install) {
+
+        // Ensure mastership on device
+        if (!mastershipService.isLocalMaster(deviceId)) {
+            return;
         }
-        Set<PortNumber> newPorts = Sets.newHashSet(ports);
-        srService.getPairLocalPort(deviceId).ifPresent(newPorts::add);
-        return newPorts;
+
+        // Locate L2 flooding group details for given xconnect vlan
+        int nextId = getNextId(deviceId, vlanId);
+        if (nextId == -1) {
+            log.debug("XConnect vlan {} broadcast group for device {} doesn't exists. " +
+                              "Aborting pair group linking.", vlanId, deviceId);
+            return;
+        }
+
+        // Add pairing-port group to flooding group
+        TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
+        // treatment.popVlan();
+        treatment.setOutput(port);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) ->
+                        log.debug("Pair port added/removed to vlan {} next objective {} on {}",
+                                  vlanId, nextId, deviceId),
+                (objective, error) ->
+                        log.warn("Failed adding/removing pair port to vlan {} next objective {} on {}." +
+                                         "Error : {}", vlanId, nextId, deviceId, error)
+        );
+        NextObjective.Builder vlanNextObjectiveBuilder = DefaultNextObjective.builder()
+                .withId(nextId)
+                .withType(NextObjective.Type.BROADCAST)
+                .fromApp(srService.appId())
+                .withMeta(DefaultTrafficSelector.builder().matchVlanId(vlanId).build())
+                .addTreatment(treatment.build());
+        if (install) {
+            flowObjectiveService.next(deviceId, vlanNextObjectiveBuilder.addToExisting(context));
+        } else {
+            flowObjectiveService.next(deviceId, vlanNextObjectiveBuilder.removeFromExisting(context));
+        }
+        log.debug("Submitted next objective {} for vlan: {} in device {}",
+                  nextId, vlanId, deviceId);
     }
+
+    /**
+     * Populate L2 multicast rule on given deviceId that matches given mac, given vlan and
+     * output to given port's L2 mulitcast group.
+     *
+     * @param deviceId    Device ID
+     * @param pairPort    Pair port number
+     * @param vlanId      VLAN ID
+     * @param accessPorts List of access ports to be added into L2 multicast group
+     */
+    private void populateL2Multicast(DeviceId deviceId, final PortNumber pairPort,
+                             VlanId vlanId, List<PortNumber> accessPorts) {
+
+        boolean multicastGroupExists = true;
+        int vlanMulticastNextId;
+
+        // Ensure enough rights to program pair device
+        if (!srService.shouldProgram(deviceId)) {
+            return;
+        }
+
+        // Step 1 : Populate single homed access ports into vlan's L2 multicast group
+        NextObjective.Builder vlanMulticastNextObjBuilder = DefaultNextObjective
+                .builder()
+                .withType(NextObjective.Type.BROADCAST)
+                .fromApp(srService.appId())
+            .withMeta(DefaultTrafficSelector.builder().matchVlanId(vlanId)
+                          .matchEthDst(MacAddress.IPV4_MULTICAST).build());
+        vlanMulticastNextId = getMulticastGroupNextObjectiveId(deviceId, vlanId);
+        if (vlanMulticastNextId == -1) {
+            // Vlan's L2 multicast group doesn't exist; create it, update store and add pair port as sub-group
+            multicastGroupExists = false;
+            vlanMulticastNextId = flowObjectiveService.allocateNextId();
+            addMulticastGroupNextObjectiveId(deviceId, vlanId, vlanMulticastNextId);
+            vlanMulticastNextObjBuilder.addTreatment(
+                    DefaultTrafficTreatment.builder().popVlan().setOutput(pairPort).build()
+            );
+        }
+        vlanMulticastNextObjBuilder.withId(vlanMulticastNextId);
+        final int nextId = vlanMulticastNextId;
+        accessPorts.forEach(p -> {
+            TrafficTreatment.Builder egressAction = DefaultTrafficTreatment.builder();
+            // Do vlan popup action based on interface configuration
+            if (interfaceService.getInterfacesByPort(new ConnectPoint(deviceId, p))
+                    .stream().noneMatch(i -> i.vlanTagged().contains(vlanId))) {
+                egressAction.popVlan();
+            }
+            egressAction.setOutput(p);
+            vlanMulticastNextObjBuilder.addTreatment(egressAction.build());
+            addMulticastGroupPort(deviceId, vlanId, p);
+        });
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) ->
+                        log.debug("L2 multicast group installed/updated. "
+                                          + "NextObject Id {} on {} for subnet {} ",
+                                  nextId, deviceId, vlanId),
+                (objective, error) ->
+                        log.warn("L2 multicast group failed to install/update. "
+                                         + " NextObject Id {} on {} for subnet {} : {}",
+                                 nextId, deviceId, vlanId, error)
+        );
+        if (!multicastGroupExists) {
+            flowObjectiveService.next(deviceId, vlanMulticastNextObjBuilder.add(context));
+
+            // Step 2 : Populate ACL rule; selector = vlan + pair-port, output = vlan L2 multicast group
+            TrafficSelector.Builder multicastSelector = DefaultTrafficSelector.builder();
+            multicastSelector.matchEthType(Ethernet.TYPE_VLAN);
+            multicastSelector.matchInPort(pairPort);
+            multicastSelector.matchVlanId(vlanId);
+            ForwardingObjective.Builder vlanMulticastForwardingObj = DefaultForwardingObjective.builder()
+                    .withFlag(ForwardingObjective.Flag.VERSATILE)
+                    .nextStep(vlanMulticastNextId)
+                    .withSelector(multicastSelector.build())
+                    .withPriority(100)
+                    .fromApp(srService.appId())
+                    .makePermanent();
+            context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("L2 multicasting versatile rule for device {}, port/vlan {}/{} populated",
+                                             deviceId,
+                                             pairPort,
+                                             vlanId),
+                    (objective, error) -> log.warn("Failed to populate L2 multicasting versatile rule for device {}, " +
+                                                           "ports/vlan {}/{}: {}", deviceId, pairPort, vlanId, error));
+            flowObjectiveService.forward(deviceId, vlanMulticastForwardingObj.add(context));
+        } else {
+            // L2_MULTICAST & BROADCAST are similar structure in subgroups; so going with BROADCAST type.
+            vlanMulticastNextObjBuilder.withType(NextObjective.Type.BROADCAST);
+            flowObjectiveService.next(deviceId, vlanMulticastNextObjBuilder.addToExisting(context));
+        }
+    }
+
+    /**
+     * Removes access ports from VLAN L2 multicast group on given deviceId.
+     *
+     * @param deviceId    Device ID
+     * @param pairPort    Pair port number
+     * @param vlanId      VLAN ID
+     * @param accessPorts List of access ports to be added into L2 multicast group
+     */
+    private void revokeL2Multicast(DeviceId deviceId, final PortNumber pairPort,
+                           VlanId vlanId, List<PortNumber> accessPorts) {
+
+        // Ensure enough rights to program pair device
+        if (!srService.shouldProgram(deviceId)) {
+            return;
+        }
+
+        int vlanMulticastNextId = getMulticastGroupNextObjectiveId(deviceId, vlanId);
+        if (vlanMulticastNextId == -1) {
+            return;
+        }
+        NextObjective.Builder vlanMulticastNextObjBuilder = DefaultNextObjective
+                .builder()
+                .withType(NextObjective.Type.BROADCAST)
+                .fromApp(srService.appId())
+                .withMeta(DefaultTrafficSelector.builder().matchVlanId(vlanId).build())
+                .withId(vlanMulticastNextId);
+        accessPorts.forEach(p -> {
+            TrafficTreatment.Builder egressAction = DefaultTrafficTreatment.builder();
+            // Do vlan popup action based on interface configuration
+            if (interfaceService.getInterfacesByPort(new ConnectPoint(deviceId, p))
+                    .stream().noneMatch(i -> i.vlanTagged().contains(vlanId))) {
+                egressAction.popVlan();
+            }
+            egressAction.setOutput(p);
+            vlanMulticastNextObjBuilder.addTreatment(egressAction.build());
+            removeMulticastGroupPort(deviceId, vlanId, p);
+        });
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) ->
+                        log.debug("L2 multicast group installed/updated. "
+                                          + "NextObject Id {} on {} for subnet {} ",
+                                  vlanMulticastNextId, deviceId, vlanId),
+                (objective, error) ->
+                        log.warn("L2 multicast group failed to install/update. "
+                                         + " NextObject Id {} on {} for subnet {} : {}",
+                                 vlanMulticastNextId, deviceId, vlanId, error)
+        );
+        flowObjectiveService.next(deviceId, vlanMulticastNextObjBuilder.removeFromExisting(context));
+    }
+
+    /**
+     * Cleans up VLAN L2 multicast group on given deviceId. ACL rules for the group will also be deleted.
+     * Normally multicast group is not removed if it contains access ports; which can be forced
+     * by "force" flag
+     *
+     * @param deviceId Device ID
+     * @param pairPort Pair port number
+     * @param vlanId   VLAN ID
+     * @param force    Forceful removal
+     */
+    private void cleanupL2MulticastRule(DeviceId deviceId, PortNumber pairPort, VlanId vlanId, boolean force) {
+
+        // Ensure enough rights to program pair device
+        if (!srService.shouldProgram(deviceId)) {
+            return;
+        }
+
+        // Ensure L2 multicast group doesn't contain access ports
+        if (hasAccessPortInMulticastGroup(deviceId, vlanId, pairPort) && !force) {
+            return;
+        }
+
+        // Load L2 multicast group details
+        int vlanMulticastNextId = getMulticastGroupNextObjectiveId(deviceId, vlanId);
+        if (vlanMulticastNextId == -1) {
+            return;
+        }
+
+        // Step 1 : Clear ACL rule; selector = vlan + pair-port, output = vlan L2 multicast group
+        TrafficSelector.Builder l2MulticastSelector = DefaultTrafficSelector.builder();
+        l2MulticastSelector.matchEthType(Ethernet.TYPE_VLAN);
+        l2MulticastSelector.matchInPort(pairPort);
+        l2MulticastSelector.matchVlanId(vlanId);
+        ForwardingObjective.Builder vlanMulticastForwardingObj = DefaultForwardingObjective.builder()
+                .withFlag(ForwardingObjective.Flag.VERSATILE)
+                .nextStep(vlanMulticastNextId)
+                .withSelector(l2MulticastSelector.build())
+                .withPriority(100)
+                .fromApp(srService.appId())
+                .makePermanent();
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("L2 multicasting rule for device {}, port/vlan {}/{} deleted", deviceId,
+                                         pairPort, vlanId),
+                (objective, error) -> log.warn("Failed to delete L2 multicasting rule for device {}, " +
+                                                       "ports/vlan {}/{}: {}", deviceId, pairPort, vlanId, error));
+        flowObjectiveService.forward(deviceId, vlanMulticastForwardingObj.remove(context));
+
+        // Step 2 : Clear L2 multicast group associated with vlan
+        NextObjective.Builder l2MulticastGroupBuilder = DefaultNextObjective
+                .builder()
+                .withId(vlanMulticastNextId)
+                .withType(NextObjective.Type.BROADCAST)
+                .fromApp(srService.appId())
+            .withMeta(DefaultTrafficSelector.builder()
+                          .matchVlanId(vlanId)
+                          .matchEthDst(MacAddress.IPV4_MULTICAST).build())
+                .addTreatment(DefaultTrafficTreatment.builder().popVlan().setOutput(pairPort).build());
+        context = new DefaultObjectiveContext(
+                (objective) ->
+                        log.debug("L2 multicast group with NextObject Id {} deleted on {} for subnet {} ",
+                                  vlanMulticastNextId, deviceId, vlanId),
+                (objective, error) ->
+                        log.warn("L2 multicast group with NextObject Id {} failed to delete on {} for subnet {} : {}",
+                                 vlanMulticastNextId, deviceId, vlanId, error)
+        );
+        flowObjectiveService.next(deviceId, l2MulticastGroupBuilder.remove(context));
+
+        // Finally clear store.
+        removeMulticastGroup(deviceId, vlanId);
+    }
+
+    private boolean isMulticastGroupExists(DeviceId deviceId, VlanId vlanId) {
+        return xconnectMulticastNextStore.asJavaMap().entrySet().stream()
+                .anyMatch(e -> e.getKey().deviceId().equals(deviceId) &&
+                        e.getKey().vlanId().equals(vlanId));
+    }
+
+    private int getMulticastGroupNextObjectiveId(DeviceId deviceId, VlanId vlanId) {
+        Optional<Integer> nextId
+                = xconnectMulticastNextStore.asJavaMap().entrySet().stream()
+                .filter(e -> e.getKey().deviceId().equals(deviceId) &&
+                        e.getKey().vlanId().equals(vlanId))
+                .findFirst()
+                .map(Map.Entry::getValue);
+        return nextId.orElse(-1);
+    }
+
+    private void addMulticastGroupNextObjectiveId(DeviceId deviceId, VlanId vlanId, int nextId) {
+        if (nextId == -1) {
+            return;
+        }
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+        xconnectMulticastNextStore.put(key, nextId);
+
+        // Update port store with empty entry.
+        xconnectMulticastPortsStore.put(key, new ArrayList<PortNumber>());
+    }
+
+    private void addMulticastGroupPort(DeviceId deviceId, VlanId vlanId, PortNumber port) {
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+        List<PortNumber> ports = xconnectMulticastPortsStore.get(key).value();
+        ports.add(port);
+        xconnectMulticastPortsStore.put(key, ports);
+    }
+
+    private void removeMulticastGroupPort(DeviceId deviceId, VlanId vlanId, PortNumber port) {
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+        List<PortNumber> ports = xconnectMulticastPortsStore.get(key).value();
+        ports.remove(port);
+        xconnectMulticastPortsStore.put(key, ports);
+    }
+
+    private void removeMulticastGroup(DeviceId deviceId, VlanId vlanId) {
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+        xconnectMulticastPortsStore.remove(key);
+        xconnectMulticastNextStore.remove(key);
+    }
+
+    private boolean hasAccessPortInMulticastGroup(DeviceId deviceId, VlanId vlanId, PortNumber pairPort) {
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+        if (!xconnectMulticastPortsStore.containsKey(key)) {
+            return false;
+        }
+        List<PortNumber> ports = xconnectMulticastPortsStore.get(key).value();
+        return ports.stream().anyMatch(p -> !p.equals(pairPort));
+    }
+
 }
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java
index 396643e..c5204ce 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java
@@ -84,6 +84,8 @@
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.driver.pipeline.ofdpa.Ofdpa2Pipeline.*;
 import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.*;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.L2_MULTICAST_TYPE;
+import static org.onosproject.driver.pipeline.ofdpa.OfdpaGroupHandlerUtility.l2MulticastGroupKey;
 import static org.onosproject.net.flow.criteria.Criterion.Type.TUNNEL_ID;
 import static org.onosproject.net.flow.criteria.Criterion.Type.VLAN_VID;
 import static org.onosproject.net.group.GroupDescription.Type.ALL;
@@ -96,29 +98,23 @@
  */
 public class Ofdpa2GroupHandler {
     protected final Logger log = getLogger(getClass());
-
     // Services, Stores
     protected GroupService groupService;
     protected StorageService storageService;
     protected FlowObjectiveStore flowObjectiveStore;
-
     // index number for group creation
     private AtomicCounter nextIndex;
-
     protected DeviceId deviceId;
     Cache<GroupKey, List<OfdpaGroupHandlerUtility.OfdpaNextGroup>> pendingAddNextObjectives;
     Cache<NextObjective, List<GroupKey>> pendingRemoveNextObjectives;
     Cache<GroupKey, Set<OfdpaGroupHandlerUtility.GroupChainElem>> pendingGroups;
     ConcurrentHashMap<GroupKey, Set<NextObjective>> pendingUpdateNextObjectives;
-
     // local store for pending bucketAdds - by design there can be multiple
     // pending bucket for a group
     protected ConcurrentHashMap<Integer, Set<NextObjective>> pendingBuckets =
             new ConcurrentHashMap<>();
-
     private ScheduledExecutorService groupCheckerExecutor =
             Executors.newScheduledThreadPool(2, groupedThreads("onos/pipeliner", "ofdpa-%d", log));
-
     /**
      * Determines whether this pipeline support copy ttl instructions or not.
      *
@@ -236,6 +232,64 @@
     }
 
     /**
+     * Similar to processBroadcastNextObjective but handles L2 Multicast Next Objectives.
+     *
+     * @param nextObj  NextObjective of L2_MULTICAST with chained NextObjectives for single homed access ports
+     */
+    private void processL2MulticastNextObjective(NextObjective nextObj) {
+
+        VlanId assignedVlan = readVlanFromSelector(nextObj.meta());
+        if (assignedVlan == null) {
+            log.warn("VLAN ID required by L2 multicast next objective is missing. Aborting group creation.");
+            fail(nextObj, ObjectiveError.BADPARAMS);
+            return;
+        }
+
+        // Group info should contain only single homed hosts for a given vlanId
+        List<GroupInfo> groupInfos = prepareL2InterfaceGroup(nextObj, assignedVlan);
+        createL2MulticastGroup(nextObj, assignedVlan, groupInfos);
+    }
+
+    private void createL2MulticastGroup(NextObjective nextObj, VlanId vlanId,  List<GroupInfo> groupInfos) {
+        // Realize & represent L2 multicast group in OFDPA driver layer
+        // TODO : Need to identify significance of OfdpaNextGroup.
+        Integer l2MulticastGroupId = L2_MULTICAST_TYPE | (vlanId.toShort() << 16);
+        final GroupKey l2MulticastGroupKey = l2MulticastGroupKey(vlanId, deviceId);
+        List<Deque<GroupKey>> l2MulticastAllGroup = Lists.newArrayList();
+        groupInfos.forEach(groupInfo -> {
+            Deque<GroupKey> groupKeyChain = new ArrayDeque<>();
+            groupKeyChain.addFirst(groupInfo.innerMostGroupDesc().appCookie());
+            groupKeyChain.addFirst(l2MulticastGroupKey);
+            l2MulticastAllGroup.add(groupKeyChain);
+        });
+        OfdpaNextGroup ofdpaL2MulticastGroup = new OfdpaNextGroup(l2MulticastAllGroup, nextObj);
+        updatePendingNextObjective(l2MulticastGroupKey, ofdpaL2MulticastGroup);
+        // Group Chain Hierarchy creation using group service and thus in device level
+        List<GroupBucket> l2McastBuckets = new ArrayList<>();
+        groupInfos.forEach(groupInfo -> {
+            // Points to L2 interface group directly.
+            TrafficTreatment.Builder trafficTreatment = DefaultTrafficTreatment.builder();
+            trafficTreatment.group(new GroupId(groupInfo.innerMostGroupDesc().givenGroupId()));
+            GroupBucket bucket = DefaultGroupBucket.createAllGroupBucket(trafficTreatment.build());
+            l2McastBuckets.add(bucket);
+        });
+        GroupDescription l2MulticastGroupDescription =
+                new DefaultGroupDescription(
+                        deviceId,
+                        ALL,
+                        new GroupBuckets(l2McastBuckets),
+                        l2MulticastGroupKey,
+                        l2MulticastGroupId,
+                        nextObj.appId());
+        GroupChainElem l2MulticastGce = new GroupChainElem(l2MulticastGroupDescription,
+                                                           groupInfos.size(), false, deviceId);
+        groupInfos.forEach(groupInfo -> {
+            updatePendingGroups(groupInfo.innerMostGroupDesc().appCookie(), l2MulticastGce);
+            groupService.addGroup(groupInfo.innerMostGroupDesc());
+        });
+    }
+
+    /**
      * As per the OFDPA 2.0 TTP, packets are sent out of ports by using
      * a chain of groups. The simple Next Objective passed in by the application
      * is broken up into a group chain. The following chains can be created
@@ -269,12 +323,10 @@
                 }
             }
         }
-
         if (plainL2) {
             createL2InterfaceGroup(nextObj);
             return;
         }
-
         // In order to understand if it is a pseudowire related
         // next objective we look for the tunnel id in the meta.
         boolean isPw = false;
@@ -286,10 +338,8 @@
                 isPw = true;
             }
         }
-
         if (mplsSwap && !isPw) {
             log.debug("Creating a MPLS Swap -> MPLS Interface -> L2 Interface group chain.");
-
             // break up simple next objective to GroupChain objects
             GroupInfo groupInfo = createL2L3Chain(treatment, nextObj.id(),
                                                   nextObj.appId(), true,
@@ -299,15 +349,12 @@
                 fail(nextObj, ObjectiveError.BADPARAMS);
                 return;
             }
-
             Deque<GroupKey> gkeyChain = new ArrayDeque<>();
             gkeyChain.addFirst(groupInfo.innerMostGroupDesc().appCookie()); // l2 interface
             gkeyChain.addFirst(groupInfo.nextGroupDesc().appCookie()); // mpls interface
-
             // creating the mpls swap group and adding it to the chain
             int nextGid = groupInfo.nextGroupDesc().givenGroupId();
             int index = getNextAvailableIndex();
-
             GroupDescription swapGroupDescription = createMplsSwap(
                     nextGid,
                     OfdpaMplsGroupSubType.MPLS_SWAP_LABEL,
@@ -321,13 +368,11 @@
                                                                1, false, deviceId);
             updatePendingGroups(groupInfo.nextGroupDesc().appCookie(), swapChainElem);
             gkeyChain.addFirst(swapGroupKey);
-
             // ensure nextObjective waits on the outermost groupKey
             List<Deque<GroupKey>> allGroupKeys = Lists.newArrayList();
             allGroupKeys.add(gkeyChain);
             OfdpaNextGroup ofdpaGrp = new OfdpaNextGroup(allGroupKeys, nextObj);
             updatePendingNextObjective(swapGroupKey, ofdpaGrp);
-
             // now we are ready to send the l2 groupDescription (inner), as all the stores
             // that will get async replies have been updated. By waiting to update
             // the stores, we prevent nasty race conditions.
@@ -355,10 +400,8 @@
             List<Deque<GroupKey>> allGroupKeys = Lists.newArrayList();
             allGroupKeys.add(gkeyChain);
             OfdpaNextGroup ofdpaGrp = new OfdpaNextGroup(allGroupKeys, nextObj);
-
             // store l3groupkey with the ofdpaNextGroup for the nextObjective that depends on it
             updatePendingNextObjective(groupInfo.nextGroupDesc().appCookie(), ofdpaGrp);
-
             // now we are ready to send the l2 groupDescription (inner), as all the stores
             // that will get async replies have been updated. By waiting to update
             // the stores, we prevent nasty race conditions.
@@ -373,7 +416,6 @@
 
     /**
      * Creates a simple L2 Interface Group.
-     *
      * @param nextObj the next Objective
      */
     private void createL2InterfaceGroup(NextObjective nextObj) {
@@ -383,25 +425,19 @@
             fail(nextObj, ObjectiveError.BADPARAMS);
             return;
         }
-
         List<GroupInfo> groupInfos = prepareL2InterfaceGroup(nextObj, assignedVlan);
-
         // There is only one L2 interface group in this case
         GroupDescription l2InterfaceGroupDesc = groupInfos.get(0).innerMostGroupDesc();
-
         // Put all dependency information into allGroupKeys
         List<Deque<GroupKey>> allGroupKeys = Lists.newArrayList();
         Deque<GroupKey> gkeyChain = new ArrayDeque<>();
         gkeyChain.addFirst(l2InterfaceGroupDesc.appCookie());
         allGroupKeys.add(gkeyChain);
-
         // Point the next objective to this group
         OfdpaNextGroup ofdpaGrp = new OfdpaNextGroup(allGroupKeys, nextObj);
         updatePendingNextObjective(l2InterfaceGroupDesc.appCookie(), ofdpaGrp);
-
         // Start installing the inner-most group
-        groupService.addGroup(l2InterfaceGroupDesc);
-    }
+        groupService.addGroup(l2InterfaceGroupDesc);    }
 
     /**
      * Creates an Mpls group of type swap.
@@ -420,7 +456,6 @@
                                               ApplicationId applicationId) {
         TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
         treatment.setMpls(mplsLabel);
-
         // We point the group to the next group.
         treatment.group(new GroupId(nextGroupId));
         GroupBucket groupBucket = DefaultGroupBucket
@@ -536,7 +571,6 @@
                           ins);
             }
         }
-
         if (vlanid == null && meta != null) {
             // use metadata if available
             Criterion vidCriterion = meta.getCriterion(VLAN_VID);
@@ -553,14 +587,12 @@
                 }
             }
         }
-
         if (vlanid == null) {
             log.error("Driver cannot process an L2/L3 group chain without "
                             + "egress vlan information for dev: {} port:{}",
                     deviceId, portNum);
             return null;
         }
-
         if (!setVlan && !popVlan) {
             // untagged outgoing port
             TrafficTreatment.Builder temp = DefaultTrafficTreatment.builder();
@@ -568,7 +600,6 @@
             innerTtb.build().allInstructions().forEach(temp::add);
             innerTtb = temp;
         }
-
         // assemble information for ofdpa l2interface group
         int l2groupId = l2GroupId(vlanid, portNum);
         // a globally unique groupkey that is different for ports in the same device,
@@ -576,7 +607,6 @@
         // for the various group-types created out of the same next objective.
         int l2gk = l2InterfaceGroupKey(deviceId, vlanid, portNum);
         final GroupKey l2groupkey = new DefaultGroupKey(appKryo.serialize(l2gk));
-
         // assemble information for outer group
         GroupDescription outerGrpDesc;
         if (mpls) {
@@ -623,11 +653,9 @@
                     deviceId, Integer.toHexString(l3groupId),
                     l3groupkey, nextId);
         }
-
         // store l2groupkey with the groupChainElem for the outer-group that depends on it
         GroupChainElem gce = new GroupChainElem(outerGrpDesc, 1, false, deviceId);
         updatePendingGroups(l2groupkey, gce);
-
         // create group description for the inner l2 interface group
         GroupBucket l2InterfaceGroupBucket =
                 DefaultGroupBucket.createIndirectGroupBucket(innerTtb.build());
@@ -642,7 +670,6 @@
                 deviceId, Integer.toHexString(l2groupId),
                 l2groupkey, nextId);
         return new GroupInfo(l2groupDescription, outerGrpDesc);
-
     }
 
     /**
@@ -661,8 +688,13 @@
             return;
         }
 
-        List<GroupInfo> groupInfos = prepareL2InterfaceGroup(nextObj, assignedVlan);
+        // Handling L2 multicast cases.
+        MacAddress dstMac = readEthDstFromSelector(nextObj.meta());
+        if (dstMac != null && dstMac.isMulticast()) {
+            processL2MulticastNextObjective(nextObj);
+        }
 
+        List<GroupInfo> groupInfos = prepareL2InterfaceGroup(nextObj, assignedVlan);
         IpPrefix ipDst = readIpDstFromSelector(nextObj.meta());
         if (ipDst != null) {
             if (ipDst.isMulticast()) {
@@ -679,10 +711,8 @@
     private List<GroupInfo> prepareL2InterfaceGroup(NextObjective nextObj,
                                                     VlanId assignedVlan) {
         ImmutableList.Builder<GroupInfo> groupInfoBuilder = ImmutableList.builder();
-
         // break up broadcast next objective to multiple groups
         Collection<TrafficTreatment> buckets = nextObj.next();
-
         // each treatment is converted to an L2 interface group
         for (TrafficTreatment treatment : buckets) {
             TrafficTreatment.Builder newTreatment = DefaultTrafficTreatment.builder();
@@ -712,12 +742,10 @@
                             " broadcast nextObjective", ins.type());
                 }
             }
-
             if (portNum == null) {
                 log.warn("Can't find output port for the bucket {}.", treatment);
                 continue;
             }
-
             // assemble info for l2 interface group
             VlanId l2InterfaceGroupVlan =
                     (egressVlan != null && !assignedVlan.equals(egressVlan)) ?
@@ -741,7 +769,6 @@
             log.debug("Trying L2-Interface: device:{} gid:{} gkey:{} nextid:{}",
                     deviceId, Integer.toHexString(l2InterfaceGroupId),
                     l2InterfaceGroupKey, nextObj.id());
-
             groupInfoBuilder.add(new GroupInfo(l2InterfaceGroupDescription,
                     l2InterfaceGroupDescription));
         }
@@ -754,7 +781,6 @@
         // group for a vlan, its index is always the same - 0
         Integer l2FloodGroupId = L2_FLOOD_TYPE | (vlanId.toShort() << 16);
         final GroupKey l2FloodGroupKey = l2FloodGroupKey(vlanId, deviceId);
-
         // collection of group buckets pointing to all the l2 interface groups
         List<GroupBucket> l2floodBuckets = generateNextGroupBuckets(groupInfos, ALL);
         // create the l2flood group-description to wait for all the
@@ -770,7 +796,6 @@
         log.debug("Trying L2-Flood: device:{} gid:{} gkey:{} nextid:{}",
                 deviceId, Integer.toHexString(l2FloodGroupId),
                 l2FloodGroupKey, nextObj.id());
-
         // Put all dependency information into allGroupKeys
         List<Deque<GroupKey>> allGroupKeys = Lists.newArrayList();
         groupInfos.forEach(groupInfo -> {
@@ -780,11 +805,9 @@
             groupKeyChain.addFirst(l2FloodGroupKey);
             allGroupKeys.add(groupKeyChain);
         });
-
         // Point the next objective to this group
         OfdpaNextGroup ofdpaGrp = new OfdpaNextGroup(allGroupKeys, nextObj);
         updatePendingNextObjective(l2FloodGroupKey, ofdpaGrp);
-
         GroupChainElem gce = new GroupChainElem(l2floodGroupDescription,
                 groupInfos.size(), false, deviceId);
         groupInfos.forEach(groupInfo -> {
@@ -825,24 +848,20 @@
             gkeyChain.addFirst(l3MulticastGroupKey);
             allGroupKeys.add(gkeyChain);
         });
-
         // Point the next objective to this group
         OfdpaNextGroup ofdpaGrp = new OfdpaNextGroup(allGroupKeys, nextObj);
         updatePendingNextObjective(l3MulticastGroupKey, ofdpaGrp);
-
         GroupChainElem outerGce = new GroupChainElem(l3MulticastGroupDesc,
                 groupInfos.size(), false, deviceId);
         groupInfos.forEach(groupInfo -> {
             // Point this group (L3 multicast) to the next group
             updatePendingGroups(groupInfo.nextGroupDesc().appCookie(), outerGce);
-
             // Point next group to inner-most group, if any
             if (!groupInfo.nextGroupDesc().equals(groupInfo.innerMostGroupDesc())) {
                 GroupChainElem innerGce = new GroupChainElem(groupInfo.nextGroupDesc(),
                         1, false, deviceId);
                 updatePendingGroups(groupInfo.innerMostGroupDesc().appCookie(), innerGce);
             }
-
             // Start installing the inner-most group
             groupService.addGroup(groupInfo.innerMostGroupDesc());
         });
@@ -868,7 +887,6 @@
         List<Deque<GroupKey>> allGroupKeys = new ArrayList<>();
         List<GroupInfo> unsentGroups = new ArrayList<>();
         createHashBucketChains(nextObj, allGroupKeys, unsentGroups);
-
         // now we can create the outermost L3 ECMP group
         List<GroupBucket> l3ecmpGroupBuckets = new ArrayList<>();
         for (GroupInfo gi : unsentGroups) {
@@ -898,11 +916,9 @@
         // create objects for local and distributed storage
         allGroupKeys.forEach(gKeyChain -> gKeyChain.addFirst(l3ecmpGroupKey));
         OfdpaNextGroup ofdpaGrp = new OfdpaNextGroup(allGroupKeys, nextObj);
-
         // store l3ecmpGroupKey with the ofdpaGroupChain for the nextObjective
         // that depends on it
         updatePendingNextObjective(l3ecmpGroupKey, ofdpaGrp);
-
         log.debug("Trying L3ECMP: device:{} gid:{} gkey:{} nextId:{}",
                 deviceId, Integer.toHexString(l3ecmpGroupId),
                 l3ecmpGroupKey, nextObj.id());
@@ -952,7 +968,6 @@
                     }
                 }
             }
-
             Deque<GroupKey> gKeyChain = new ArrayDeque<>();
             // here we only deal with 0 and 1 label push
             if (labelsPushed == 0) {
@@ -980,11 +995,9 @@
                 }
                 gKeyChain.addFirst(noLabelGroupInfo.innerMostGroupDesc().appCookie());
                 gKeyChain.addFirst(noLabelGroupInfo.nextGroupDesc().appCookie());
-
                 // we can't send the inner group description yet, as we have to
                 // create the dependent ECMP group first. So we store..
                 unsentGroups.add(noLabelGroupInfo);
-
             } else if (labelsPushed == 1) {
                 GroupInfo onelabelGroupInfo = createL2L3Chain(bucket, nextObj.id(),
                         nextObj.appId(), true,
@@ -1011,7 +1024,6 @@
                 if (requireVlanPopBeforeMplsPush()) {
                     l3vpnTtb.pushVlan().setVlanId(VlanId.vlanId(VlanId.RESERVED));
                 }
-
                 GroupBucket l3vpnGrpBkt  =
                         DefaultGroupBucket.createIndirectGroupBucket(l3vpnTtb.build());
                 int l3vpnIndex = getNextAvailableIndex();
@@ -1035,24 +1047,19 @@
                 gKeyChain.addFirst(onelabelGroupInfo.innerMostGroupDesc().appCookie());
                 gKeyChain.addFirst(onelabelGroupInfo.nextGroupDesc().appCookie());
                 gKeyChain.addFirst(l3vpnGroupKey);
-
                 //now we can replace the outerGrpDesc with the one we just created
                 onelabelGroupInfo.nextGroupDesc(l3vpnGroupDesc);
-
                 // we can't send the innermost group yet, as we have to create
                 // the dependent ECMP group first. So we store ...
                 unsentGroups.add(onelabelGroupInfo);
-
                 log.debug("Trying L3VPN: device:{} gid:{} group key:{} nextId:{}",
                         deviceId, Integer.toHexString(l3vpnGroupId),
                         l3vpnGroupKey, nextObj.id());
-
             } else {
                 log.warn("Driver currently does not handle more than 1 MPLS "
                         + "labels. Not processing nextObjective {}", nextObj.id());
                 return;
             }
-
             // all groups in this chain
             allGroupKeys.add(gKeyChain);
         }
@@ -1089,7 +1096,6 @@
             fail(nextObjective, ObjectiveError.UNSUPPORTED);
             return;
         }
-
         // first check to see if bucket being added is not a duplicate of an
         // existing bucket. If it is for an existing output port, then its a
         // duplicate.
@@ -1100,7 +1106,6 @@
                                                                deviceId);
         Set<TrafficTreatment> nonDuplicateBuckets = Sets.newHashSet();
         NextObjective objectiveToAdd;
-
         nextObjective.next().forEach(trafficTreatment -> {
             PortNumber portNumber = readOutPortFromTreatment(trafficTreatment);
             if (portNumber == null) {
@@ -1125,7 +1130,6 @@
                 nonDuplicateBuckets.add(trafficTreatment);
             }
         });
-
         if (duplicateBuckets.isEmpty()) {
             // use the original objective
             objectiveToAdd = nextObjective;
@@ -1140,7 +1144,6 @@
                     .withMeta(nextObjective.meta())
                     .fromApp(nextObjective.appId());
             nonDuplicateBuckets.forEach(builder::addTreatment);
-
             ObjectiveContext context = nextObjective.context().orElse(null);
             objectiveToAdd = builder.addToExisting(context);
         } else {
@@ -1150,7 +1153,6 @@
             pass(nextObjective);
             return;
         }
-
         if (nextObjective.type() == NextObjective.Type.HASHED) {
             addBucketToHashGroup(objectiveToAdd, allActiveKeys);
         } else if (nextObjective.type() == NextObjective.Type.BROADCAST) {
@@ -1165,10 +1167,8 @@
         List<GroupInfo> unsentGroups = new ArrayList<>();
         List<GroupBucket> newBuckets;
         createHashBucketChains(nextObjective, allGroupKeys, unsentGroups);
-
         // now we can create the buckets to add to the outermost L3 ECMP group
         newBuckets = generateNextGroupBuckets(unsentGroups, SELECT);
-
         // retrieve the original L3 ECMP group
         Group l3ecmpGroup = retrieveTopLevelGroup(allActiveKeys, deviceId,
                                                   groupService, nextObjective.id());
@@ -1178,7 +1178,6 @@
         }
         GroupKey l3ecmpGroupKey = l3ecmpGroup.appCookie();
         int l3ecmpGroupId = l3ecmpGroup.id().id();
-
         // Although GroupDescriptions are not necessary for adding buckets to
         // existing groups, we still use one in the GroupChainElem. When the latter is
         // processed, the info will be extracted for the bucketAdd call to groupService
@@ -1205,7 +1204,6 @@
         log.debug("Adding to L3ECMP: device:{} gid:{} group key:{} nextId:{}",
                 deviceId, Integer.toHexString(l3ecmpGroupId),
                 l3ecmpGroupKey, nextObjective.id());
-
         unsentGroups.forEach(groupInfo -> {
             // send the innermost group
             log.debug("Sending innermost group {} in group chain on device {} ",
@@ -1257,7 +1255,6 @@
         GroupKey l2floodGroupKey = l2FloodGroup.appCookie();
         int l2floodGroupId = l2FloodGroup.id().id();
         List<GroupBucket> newBuckets = generateNextGroupBuckets(groupInfos, ALL);
-
         GroupDescription l2FloodGroupDescription =
                 new DefaultGroupDescription(deviceId,
                                             ALL,
@@ -1265,7 +1262,6 @@
                                             l2floodGroupKey,
                                             l2floodGroupId,
                                             nextObj.appId());
-
         GroupChainElem l2FloodGroupChainElement =
                 new GroupChainElem(l2FloodGroupDescription,
                                    groupInfos.size(),
@@ -1275,7 +1271,6 @@
 
         //ensure assignedVlan applies to the chosen group
         VlanId floodGroupVlan = extractVlanIdFromGroupId(l2floodGroupId);
-
         if (!floodGroupVlan.equals(assignedVlan)) {
             log.warn("VLAN ID {} does not match Flood group {} to which bucket is "
                              + "being added, for next:{} in dev:{}. Abort.", assignedVlan,
@@ -1290,7 +1285,6 @@
             newBucketChain.addFirst(groupInfo.nextGroupDesc().appCookie());
             newBucketChain.addFirst(l2floodGroupKey);
             addedKeys.add(newBucketChain);
-
             log.debug("Adding to L2FLOOD: device:{} gid:{} group key:{} nextId:{}",
                       deviceId, Integer.toHexString(l2floodGroupId),
                       l2floodGroupKey, nextObj.id());
@@ -1298,7 +1292,6 @@
             log.debug("Sending innermost group {} in group chain on device {} ",
                       Integer.toHexString(groupInfo.innerMostGroupDesc().givenGroupId()),
                       deviceId);
-
             updatePendingGroups(groupInfo.nextGroupDesc().appCookie(), l2FloodGroupChainElement);
 
             DeviceId innerMostGroupDevice = groupInfo.innerMostGroupDesc().deviceId();
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java
index 73d1d7d..eae78fd 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java
@@ -56,6 +56,8 @@
 import org.onosproject.net.flow.criteria.Criterion;
 import org.onosproject.net.flow.criteria.EthCriterion;
 import org.onosproject.net.flow.criteria.EthTypeCriterion;
+import org.onosproject.net.flow.criteria.ExtensionCriterion;
+import org.onosproject.net.flow.criteria.ExtensionSelector;
 import org.onosproject.net.flow.criteria.IPCriterion;
 import org.onosproject.net.flow.criteria.Icmpv6CodeCriterion;
 import org.onosproject.net.flow.criteria.Icmpv6TypeCriterion;
@@ -1851,6 +1853,15 @@
                 ? null : ((VlanIdCriterion) criterion).vlanId();
     }
 
+    static MacAddress readEthDstFromSelector(TrafficSelector selector) {
+        if (selector == null) {
+            return null;
+        }
+        Criterion criterion = selector.getCriterion(Criterion.Type.ETH_DST);
+        return (criterion == null)
+                ? null : ((EthCriterion) criterion).mac();
+    }
+
     static IpPrefix readIpDstFromSelector(TrafficSelector selector) {
         if (selector == null) {
             return null;
@@ -1886,6 +1897,14 @@
         return null;
     }
 
+    static ExtensionSelector readExtensionFromSelector(TrafficSelector selector) {
+        if (selector == null) {
+            return null;
+        }
+        ExtensionCriterion criterion = (ExtensionCriterion) selector.getCriterion(Criterion.Type.EXTENSION);
+        return (criterion == null) ? null : criterion.extensionSelector();
+    }
+
     /**
      *  Utility class that retries sending flows a fixed number of times, even if
      *  some of the attempts are successful. Used only for forwarding objectives.
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
index 31b92a8..5e48ebf 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
@@ -77,6 +77,7 @@
     static final int MPLS_L3VPN_SUBTYPE = 0x92000000;
     static final int L3_ECMP_TYPE = 0x70000000;
     static final int L2_FLOOD_TYPE = 0x40000000;
+    static final int L2_MULTICAST_TYPE = 0x30000000;
 
     static final int TYPE_MASK = 0x0fffffff;
     static final int SUBTYPE_MASK = 0x00ffffff;
@@ -453,6 +454,12 @@
         return new DefaultGroupKey(Ofdpa2Pipeline.appKryo.serialize(hash));
     }
 
+    public static GroupKey l2MulticastGroupKey(VlanId vlanId, DeviceId deviceId) {
+        int hash = Objects.hash(deviceId, vlanId);
+        hash = L2_MULTICAST_TYPE | TYPE_MASK & hash;
+        return new DefaultGroupKey(Ofdpa2Pipeline.appKryo.serialize(hash));
+    }
+
     public static int l2GroupId(VlanId vlanId, long portNum) {
         return L2_INTERFACE_TYPE | (vlanId.toShort() << 16) | (int) portNum;
     }