CORD-81 Support for editing buckets of ALL groups, both L2Flood and
L3Multicast in the OF-DPA driver. Additionally changed SR app to
react to edge ports going up/down by sending flood group edit calls.

Change-Id: I6d7749f579d3d1d800e6ca8ce2316bd0e3f6136b
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 acb7a7c..7da0be5 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
@@ -495,7 +495,7 @@
      * As per the OFDPA 2.0 TTP, packets are sent out of ports by using
      * a chain of groups. The broadcast Next Objective passed in by the application
      * has to be broken up into a group chain comprising of an
-     * L2 Flood group whose buckets point to L2 Interface groups.
+     * L2 Flood group or L3 Multicast group, whose buckets point to L2 Interface groups.
      *
      * @param nextObj  the nextObjective of type BROADCAST
      */
@@ -905,13 +905,17 @@
      * @param next the representation of the existing group-chain for this next objective
      */
     protected void addBucketToGroup(NextObjective nextObjective, NextGroup next) {
-        if (nextObjective.type() != NextObjective.Type.HASHED) {
+        if (nextObjective.type() != NextObjective.Type.HASHED &&
+                nextObjective.type() != NextObjective.Type.BROADCAST) {
             log.warn("AddBuckets not applied to nextType:{} in dev:{} for next:{}",
                     nextObjective.type(), deviceId, nextObjective.id());
+            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.UNSUPPORTED);
             return;
         }
         if (nextObjective.next().size() > 1) {
+            // FIXME - support editing multiple buckets CORD-555
             log.warn("Only one bucket can be added at a time");
+            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.UNSUPPORTED);
             return;
         }
         // first check to see if bucket being added is not a duplicate of an
@@ -937,36 +941,34 @@
                      newport, deviceId, nextObjective.id());
             return;
         }
+        if (nextObjective.type() == NextObjective.Type.HASHED) {
+            addBucketToHashGroup(nextObjective, allActiveKeys, newport);
+        } else if (nextObjective.type() == NextObjective.Type.BROADCAST) {
+            addBucketToBroadcastGroup(nextObjective, allActiveKeys, newport);
+        }
+    }
 
+    private void addBucketToHashGroup(NextObjective nextObjective,
+                                      List<Deque<GroupKey>> allActiveKeys,
+                                      PortNumber newport) {
         // storage for all group keys in the chain of groups created
         List<Deque<GroupKey>> allGroupKeys = new ArrayList<>();
         List<GroupInfo> unsentGroups = new ArrayList<>();
         createHashBucketChains(nextObjective, allGroupKeys, unsentGroups);
 
-        // now we can create the outermost L3 ECMP group bucket to add
+        // now we can create the bucket to add to the outermost L3 ECMP group
         GroupInfo gi = unsentGroups.get(0); // only one bucket, so only one group-chain
         TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
         ttb.group(new DefaultGroupId(gi.nextGroupDesc.givenGroupId()));
         GroupBucket sbucket = DefaultGroupBucket.createSelectGroupBucket(ttb.build());
 
-        // retrieve the original L3 ECMP group id and description from the first
-        // element in any deque.
-        GroupKey l3ecmpGroupKey = null;
-        if (!allActiveKeys.isEmpty()) {
-            l3ecmpGroupKey = allActiveKeys.get(0).peekFirst();
-        } else {
-            log.warn("Could not determine top level group while trying to"
-                    + "add bucket for port:{} in dev:{} for next:{}",
-                    deviceId, nextObjective.id(), newport);
-            return;
-        }
-        Group l3ecmpGroup = groupService.getGroup(deviceId, l3ecmpGroupKey);
+        // retrieve the original L3 ECMP group
+        Group l3ecmpGroup = retrieveTopLevelGroup(allActiveKeys, nextObjective.id());
         if (l3ecmpGroup == null) {
-            log.warn("Could not find l3 ecmp group while trying to add bucket"
-                    + "for port:{} in dev:{} for next:{}", deviceId,
-                    nextObjective.id(), newport);
+            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.GROUPMISSING);
             return;
         }
+        GroupKey l3ecmpGroupKey = l3ecmpGroup.appCookie();
         int l3ecmpGroupId = l3ecmpGroup.id().id();
 
         // Although GroupDescriptions are not necessary for adding buckets to
@@ -1001,21 +1003,190 @@
                 Integer.toHexString(gi.innerMostGroupDesc.givenGroupId()), deviceId);
         updatePendingGroups(gi.nextGroupDesc.appCookie(), l3ecmpGce);
         groupService.addGroup(gi.innerMostGroupDesc);
+    }
 
+    private void addBucketToBroadcastGroup(NextObjective nextObj,
+                                        List<Deque<GroupKey>> allActiveKeys,
+                                        PortNumber newport) {
+        VlanId assignedVlan = Ofdpa2Pipeline.readVlanFromSelector(nextObj.meta());
+        if (assignedVlan == null) {
+            log.warn("VLAN ID required by broadcast next obj is missing. "
+                    + "Aborting add bucket to broadcast group for next:{} in dev:{}",
+                    nextObj.id(), deviceId);
+            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
+            return;
+        }
+
+        List<GroupInfo> groupInfos = prepareL2InterfaceGroup(nextObj, assignedVlan);
+
+        IpPrefix ipDst = Ofdpa2Pipeline.readIpDstFromSelector(nextObj.meta());
+        if (ipDst != null) {
+            if (ipDst.isMulticast()) {
+                addBucketToL3MulticastGroup(nextObj, allActiveKeys,
+                                            groupInfos, assignedVlan, newport);
+            } else {
+                log.warn("Broadcast NextObj with non-multicast IP address {}", nextObj);
+                Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
+                return;
+            }
+        } else {
+            addBucketToL2FloodGroup(nextObj, allActiveKeys,
+                                    groupInfos, assignedVlan, newport);
+        }
+    }
+
+    private void addBucketToL2FloodGroup(NextObjective nextObj,
+                                         List<Deque<GroupKey>> allActiveKeys,
+                                         List<GroupInfo> groupInfos,
+                                         VlanId assignedVlan,
+                                         PortNumber newport) {
+        // create the bucket to add to the outermost L2 Flood group
+        GroupInfo groupInfo = groupInfos.get(0); // only one bucket to add
+        GroupDescription l2intGrpDesc = groupInfo.nextGroupDesc;
+        TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
+        ttb.group(new DefaultGroupId(l2intGrpDesc.givenGroupId()));
+        GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
+        // get the group being edited
+        Group l2floodGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
+        if (l2floodGroup == null) {
+            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.GROUPMISSING);
+            return;
+        }
+        GroupKey l2floodGroupKey = l2floodGroup.appCookie();
+        int l2floodGroupId = l2floodGroup.id().id();
+
+        //ensure assignedVlan applies to the chosen group
+        VlanId expectedVlan = VlanId.vlanId((short) ((l2floodGroupId & 0x0fff0000) >> 16));
+        if (!expectedVlan.equals(assignedVlan)) {
+            log.warn("VLAN ID {} does not match Flood group {} to which bucket is "
+                    + "being added, for next:{} in dev:{}. Abort.", assignedVlan,
+                    Integer.toHexString(l2floodGroupId), nextObj.id(), deviceId);
+            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
+        }
+        GroupDescription l2floodGroupDescription =
+                new DefaultGroupDescription(
+                        deviceId,
+                        GroupDescription.Type.ALL,
+                        new GroupBuckets(Collections.singletonList(abucket)),
+                        l2floodGroupKey,
+                        l2floodGroupId,
+                        nextObj.appId());
+        GroupChainElem l2floodGce = new GroupChainElem(l2floodGroupDescription, 1, true);
+
+        // update original NextGroup with new bucket-chain
+        // If active keys shows only the top-level group without a chain of groups,
+        // then it represents an empty group. Update by replacing empty chain.
+        Deque<GroupKey> newBucketChain = new ArrayDeque<>();
+        newBucketChain.addFirst(groupInfo.nextGroupDesc.appCookie());
+        newBucketChain.addFirst(l2floodGroupKey);
+        if (allActiveKeys.size() == 1 && allActiveKeys.get(0).size() == 1) {
+            allActiveKeys.clear();
+        }
+        allActiveKeys.add(newBucketChain);
+        updatePendingNextObjective(l2floodGroupKey,
+                                   new OfdpaNextGroup(allActiveKeys, nextObj));
+        log.debug("Adding to L2FLOOD: device:{} gid:{} gkey:{} nextId:{}",
+                  deviceId, Integer.toHexString(l2floodGroupId),
+                  l2floodGroupKey, nextObj.id());
+        // send the innermost group
+        log.debug("Sending innermost group {} in group chain on device {} ",
+                Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()),
+                deviceId);
+        updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l2floodGce);
+        groupService.addGroup(groupInfo.innerMostGroupDesc);
+    }
+
+    private void addBucketToL3MulticastGroup(NextObjective nextObj,
+                                             List<Deque<GroupKey>> allActiveKeys,
+                                             List<GroupInfo> groupInfos,
+                                             VlanId assignedVlan,
+                                             PortNumber newport) {
+        // create the bucket to add to the outermost L3 Multicast group
+        GroupInfo groupInfo = groupInfos.get(0); // only one bucket to add
+        // Points to L3 interface group if there is one.
+        // Otherwise points to L2 interface group directly.
+        GroupDescription nextGroupDesc = (groupInfo.nextGroupDesc != null) ?
+                groupInfo.nextGroupDesc : groupInfo.innerMostGroupDesc;
+        TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
+        ttb.group(new DefaultGroupId(nextGroupDesc.givenGroupId()));
+        GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
+
+        // get the group being edited
+        Group l3mcastGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
+        if (l3mcastGroup == null) {
+            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.GROUPMISSING);
+            return;
+        }
+        GroupKey l3mcastGroupKey = l3mcastGroup.appCookie();
+        int l3mcastGroupId = l3mcastGroup.id().id();
+
+        //ensure assignedVlan applies to the chosen group
+        VlanId expectedVlan = VlanId.vlanId((short) ((l3mcastGroupId & 0x0fff0000) >> 16));
+        if (!expectedVlan.equals(assignedVlan)) {
+            log.warn("VLAN ID {} does not match L3 Mcast group {} to which bucket is "
+                    + "being added, for next:{} in dev:{}. Abort.", assignedVlan,
+                    Integer.toHexString(l3mcastGroupId), nextObj.id(), deviceId);
+            Ofdpa2Pipeline.fail(nextObj, ObjectiveError.BADPARAMS);
+        }
+        GroupDescription l3mcastGroupDescription =
+                new DefaultGroupDescription(
+                        deviceId,
+                        GroupDescription.Type.ALL,
+                        new GroupBuckets(Collections.singletonList(abucket)),
+                        l3mcastGroupKey,
+                        l3mcastGroupId,
+                        nextObj.appId());
+        GroupChainElem l3mcastGce = new GroupChainElem(l3mcastGroupDescription,
+                                                       1, true);
+
+        // update original NextGroup with new bucket-chain
+        Deque<GroupKey> newBucketChain = new ArrayDeque<>();
+        newBucketChain.addFirst(groupInfo.innerMostGroupDesc.appCookie());
+        // Add L3 interface group to the chain if there is one.
+        if (!groupInfo.nextGroupDesc.equals(groupInfo.innerMostGroupDesc)) {
+            newBucketChain.addFirst(groupInfo.nextGroupDesc.appCookie());
+        }
+        newBucketChain.addFirst(l3mcastGroupKey);
+        // If active keys shows only the top-level group without a chain of groups,
+        // then it represents an empty group. Update by replacing empty chain.
+        if (allActiveKeys.size() == 1 && allActiveKeys.get(0).size() == 1) {
+            allActiveKeys.clear();
+        }
+        allActiveKeys.add(newBucketChain);
+        updatePendingNextObjective(l3mcastGroupKey,
+                                   new OfdpaNextGroup(allActiveKeys, nextObj));
+
+        updatePendingGroups(groupInfo.nextGroupDesc.appCookie(), l3mcastGce);
+        // Point next group to inner-most group, if any
+        if (!groupInfo.nextGroupDesc.equals(groupInfo.innerMostGroupDesc)) {
+            GroupChainElem innerGce = new GroupChainElem(groupInfo.nextGroupDesc,
+                    1, false);
+            updatePendingGroups(groupInfo.innerMostGroupDesc.appCookie(), innerGce);
+        }
+        log.debug("Adding to L3MCAST: device:{} gid:{} gkey:{} nextId:{}",
+                  deviceId, Integer.toHexString(l3mcastGroupId),
+                  l3mcastGroupKey, nextObj.id());
+        // send the innermost group
+        log.debug("Sending innermost group {} in group chain on device {} ",
+                Integer.toHexString(groupInfo.innerMostGroupDesc.givenGroupId()),
+                deviceId);
+        groupService.addGroup(groupInfo.innerMostGroupDesc);
     }
 
     /**
      * Removes the bucket in the top level group of a possible group-chain. Does
-     * not remove the groups in a group-chain pointed to by this bucket, as they
+     * not remove the groups in the group-chain pointed to by this bucket, as they
      * may be in use (referenced by other groups) elsewhere.
      *
      * @param nextObjective the bucket information for a next group
      * @param next the representation of the existing group-chain for this next objective
      */
     protected void removeBucketFromGroup(NextObjective nextObjective, NextGroup next) {
-        if (nextObjective.type() != NextObjective.Type.HASHED) {
+        if (nextObjective.type() != NextObjective.Type.HASHED &&
+                nextObjective.type() != NextObjective.Type.BROADCAST) {
             log.warn("RemoveBuckets not applied to nextType:{} in dev:{} for next:{}",
                     nextObjective.type(), deviceId, nextObjective.id());
+            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.UNSUPPORTED);
             return;
         }
         Collection<TrafficTreatment> treatments = nextObjective.next();
@@ -1026,6 +1197,7 @@
         if (portToRemove == null) {
             log.warn("next objective {} has no outport.. cannot remove bucket"
                     + "from group in dev: {}", nextObjective.id(), deviceId);
+            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.BADPARAMS);
             return;
         }
 
@@ -1049,48 +1221,58 @@
             }
             index++;
         }
-        if (foundChain != null) {
-            //first groupkey is the one we want to modify
-            GroupKey modGroupKey = foundChain.peekFirst();
-            Group modGroup = groupService.getGroup(deviceId, modGroupKey);
-            //second groupkey is the one we wish to remove the reference to
-            GroupKey pointedGroupKey = null;
-            int i = 0;
-            for (GroupKey gk : foundChain) {
-                if (i++ == 1) {
-                    pointedGroupKey = gk;
-                    break;
-                }
-            }
-            Group pointedGroup = groupService.getGroup(deviceId, pointedGroupKey);
-            GroupBucket bucket = DefaultGroupBucket.createSelectGroupBucket(
-                    DefaultTrafficTreatment.builder()
-                            .group(pointedGroup.id())
-                            .build());
-            GroupBuckets removeBuckets = new GroupBuckets(Collections
-                    .singletonList(bucket));
-            log.debug("Removing buckets from group id 0x{} pointing to group id 0x{}"
-                    + "for next id {} in device {}", Integer.toHexString(modGroup.id().id()),
-                    Integer.toHexString(pointedGroup.id().id()), nextObjective.id(), deviceId);
-            groupService.removeBucketsFromGroup(deviceId, modGroupKey,
-                    removeBuckets, modGroupKey,
-                    nextObjective.appId());
-            // update store
-            // If the bucket removed was the last bucket in the group, then
-            // retain an entry for the top level group which still exists.
-            if (allActiveKeys.size() == 1) {
-                ArrayDeque<GroupKey> top = new ArrayDeque<>();
-                top.add(modGroupKey);
-                allActiveKeys.add(top);
-            }
-            allActiveKeys.remove(index);
-            flowObjectiveStore.putNextGroup(nextObjective.id(),
-                                            new OfdpaNextGroup(allActiveKeys,
-                                                               nextObjective));
-        } else {
+        if (foundChain == null) {
             log.warn("Could not find appropriate group-chain for removing bucket"
                     + " for next id {} in dev:{}", nextObjective.id(), deviceId);
+            Ofdpa2Pipeline.fail(nextObjective, ObjectiveError.BADPARAMS);
+            return;
         }
+
+        //first groupkey is the one we want to modify
+        GroupKey modGroupKey = foundChain.peekFirst();
+        Group modGroup = groupService.getGroup(deviceId, modGroupKey);
+        //second groupkey is the one we wish to remove the reference to
+        GroupKey pointedGroupKey = null;
+        int i = 0;
+        for (GroupKey gk : foundChain) {
+            if (i++ == 1) {
+                pointedGroupKey = gk;
+                break;
+            }
+        }
+        Group pointedGroup = groupService.getGroup(deviceId, pointedGroupKey);
+        GroupBucket bucket = null;
+        if (nextObjective.type() == NextObjective.Type.HASHED) {
+            bucket = DefaultGroupBucket.createSelectGroupBucket(
+                                            DefaultTrafficTreatment.builder()
+                                            .group(pointedGroup.id())
+                                            .build());
+        } else {
+            bucket = DefaultGroupBucket.createAllGroupBucket(
+                                            DefaultTrafficTreatment.builder()
+                                            .group(pointedGroup.id())
+                                            .build());
+        }
+        GroupBuckets removeBuckets = new GroupBuckets(Collections
+                                                      .singletonList(bucket));
+        log.debug("Removing buckets from group id 0x{} pointing to group id 0x{} "
+                + "for next id {} in device {}", Integer.toHexString(modGroup.id().id()),
+                Integer.toHexString(pointedGroup.id().id()), nextObjective.id(), deviceId);
+        groupService.removeBucketsFromGroup(deviceId, modGroupKey,
+                                            removeBuckets, modGroupKey,
+                                            nextObjective.appId());
+        // update store
+        // If the bucket removed was the last bucket in the group, then
+        // retain an entry for the top level group which still exists.
+        if (allActiveKeys.size() == 1) {
+            ArrayDeque<GroupKey> top = new ArrayDeque<>();
+            top.add(modGroupKey);
+            allActiveKeys.add(top);
+        }
+        allActiveKeys.remove(index);
+        flowObjectiveStore.putNextGroup(nextObjective.id(),
+                                        new OfdpaNextGroup(allActiveKeys,
+                                                           nextObjective));
     }
 
     /**
@@ -1288,8 +1470,34 @@
         return L2_INTERFACE_TYPE | (TYPE_MASK & hash << 6) | portLowerBits;
     }
 
+    private Group retrieveTopLevelGroup(List<Deque<GroupKey>> allActiveKeys,
+                                        int nextid) {
+        GroupKey topLevelGroupKey = null;
+        if (!allActiveKeys.isEmpty()) {
+            topLevelGroupKey = allActiveKeys.get(0).peekFirst();
+        } else {
+            log.warn("Could not determine top level group while processing"
+                    + "next:{} in dev:{}", nextid, deviceId);
+            return null;
+        }
+        Group topGroup = groupService.getGroup(deviceId, topLevelGroupKey);
+        if (topGroup == null) {
+            log.warn("Could not find top level group while processing "
+                    + "next:{} in dev:{}", nextid, deviceId);
+        }
+        return topGroup;
+    }
+
     /**
      * Utility class for moving group information around.
+     *
+     * Example: Suppose we are trying to create a group-chain A-B-C-D, where
+     * A is the top level group, and D is the inner-most group, typically L2 Interface.
+     * The innerMostGroupDesc is always D. At various stages of the creation
+     * process the nextGroupDesc may be C or B. The nextGroupDesc exists to
+     * inform the referencing group about which group it needs to point to,
+     * and wait for. In some cases the group chain may simply be A-B. In this case,
+     * both innerMostGroupDesc and nextGroupDesc will be B.
      */
     protected class GroupInfo {
         /**
@@ -1301,7 +1509,7 @@
         /**
          * Description of the next group in the group chain.
          * It can be L2 interface, L3 interface, L3 unicast, L3 VPN group.
-         * It is possible that nextGroup is the same as the innerMostGroup.
+         * It is possible that nextGroupDesc is the same as the innerMostGroup.
          */
         private GroupDescription nextGroupDesc;