[CORD-2456] Multicast support in t3

Change-Id: I3559495a695aa27cc8c8f6f3a9f465f442200e8a
diff --git a/apps/t3/src/main/java/org/onosproject/t3/cli/T3CliUtils.java b/apps/t3/src/main/java/org/onosproject/t3/cli/T3CliUtils.java
index e2375a7..30e1ca0 100644
--- a/apps/t3/src/main/java/org/onosproject/t3/cli/T3CliUtils.java
+++ b/apps/t3/src/main/java/org/onosproject/t3/cli/T3CliUtils.java
@@ -111,7 +111,8 @@
     //Prints the flows for a given trace and a specified level of verbosity
     private static StringBuilder printFlows(StaticPacketTrace trace, boolean verbose, ConnectPoint connectPoint,
                                             StringBuilder tracePrint) {
-        tracePrint.append("Flows");
+        tracePrint.append("Flows ");
+        tracePrint.append(trace.getFlowsForDevice(connectPoint.deviceId()).size());
         tracePrint.append("\n");
         trace.getFlowsForDevice(connectPoint.deviceId()).forEach(f -> {
             if (verbose) {
diff --git a/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootManager.java b/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootManager.java
index b89732d..2c41382 100644
--- a/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootManager.java
+++ b/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootManager.java
@@ -39,6 +39,7 @@
 import org.onosproject.net.config.basics.InterfaceConfig;
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.driver.DriverService;
+import org.onosproject.net.edge.EdgePortService;
 import org.onosproject.net.flow.DefaultTrafficSelector;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
@@ -86,6 +87,7 @@
 import static org.onosproject.net.flow.instructions.L2ModificationInstruction.ModMplsHeaderInstruction;
 import static org.onosproject.net.flow.instructions.L2ModificationInstruction.ModMplsLabelInstruction;
 import static org.onosproject.net.flow.instructions.L2ModificationInstruction.ModVlanIdInstruction;
+import static org.onosproject.t3.impl.TroubleshootUtils.compareMac;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -125,6 +127,9 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected NetworkConfigService networkConfigService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected EdgePortService edgePortService;
+
     @Override
     public StaticPacketTrace trace(HostId sourceHost, HostId destinationHost, EtherType etherType) {
         Host source = hostService.getHost(sourceHost);
@@ -394,7 +399,17 @@
                     //continue the trace along the path
                     getTrace(completePath, dst, trace);
                 }
-
+            } else if (edgePortService.isEdgePoint(outputPath.getOutput()) &&
+                    trace.getInitialPacket().getCriterion(Criterion.Type.ETH_DST) != null &&
+                    ((EthCriterion) trace.getInitialPacket().getCriterion(Criterion.Type.ETH_DST))
+                            .mac().isMulticast()) {
+                trace.addResultMessage("Packet is multicast and reached output " + outputPath.getOutput() +
+                        " which is enabled and is edge port");
+                computePath(completePath, trace, outputPath.getOutput());
+                completePath.clear();
+                if (!hasOtherOutput(in.deviceId(), trace, outputPath.getOutput())) {
+                    return trace;
+                }
             } else if (deviceService.getPort(cp).isEnabled()) {
                 EthTypeCriterion ethTypeCriterion = (EthTypeCriterion) trace.getInitialPacket()
                         .getCriterion(Criterion.Type.ETH_TYPE);
@@ -423,6 +438,7 @@
         return trace;
     }
 
+
     /**
      * If the initial packet comes tagged with a Vlan we output it with that to ONOS.
      * If ONOS applied a vlan we remove it.
@@ -430,6 +446,7 @@
      * @param outputPath the output
      * @param trace      the trace we are building
      */
+
     private void handleVlanToController(GroupsInDevice outputPath, StaticPacketTrace trace) {
 
         VlanIdCriterion initialVid = (VlanIdCriterion) trace.getInitialPacket().getCriterion(Criterion.Type.VLAN_VID);
@@ -450,6 +467,20 @@
     }
 
     /**
+     * Checks if the device has other outputs than the given connect point.
+     *
+     * @param inDeviceId the device
+     * @param trace      the trace we are building
+     * @param cp         an output connect point
+     * @return true if the device has other outputs.
+     */
+    private boolean hasOtherOutput(DeviceId inDeviceId, StaticPacketTrace trace, ConnectPoint cp) {
+        return trace.getGroupOuputs(inDeviceId).stream().filter(groupsInDevice -> {
+            return !groupsInDevice.getOutput().equals(cp);
+        }).count() > 0;
+    }
+
+    /**
      * Checks if the path contains the device.
      *
      * @param completePath the path
@@ -1075,28 +1106,72 @@
      * @return true if the packet matches the flow.
      */
     private boolean match(TrafficSelector packet, FlowEntry flowEntry) {
-        //TODO handle MAC matching
         return flowEntry.selector().criteria().stream().allMatch(criterion -> {
             Criterion.Type type = criterion.type();
             //If the criterion has IP we need to do LPM to establish matching.
             if (type.equals(Criterion.Type.IPV4_SRC) || type.equals(Criterion.Type.IPV4_DST) ||
                     type.equals(Criterion.Type.IPV6_SRC) || type.equals(Criterion.Type.IPV6_DST)) {
-                IPCriterion ipCriterion = (IPCriterion) criterion;
-                IPCriterion matchCriterion = (IPCriterion) packet.getCriterion(ipCriterion.type());
-                //if the packet does not have an IPv4 or IPv6 criterion we return false
-                if (matchCriterion == null) {
-                    return false;
-                }
-                try {
-                    Subnet subnet = Subnet.createInstance(ipCriterion.ip().toString());
-                    return subnet.isInSubnet(matchCriterion.ip().address().toInetAddress());
-                } catch (UnknownHostException e) {
-                    return false;
-                }
+                return matchIp(packet, (IPCriterion) criterion);
                 //we check that the packet contains the criterion provided by the flow rule.
+            } else if (type.equals(Criterion.Type.ETH_SRC_MASKED)) {
+                return matchMac(packet, (EthCriterion) criterion, false);
+            } else if (type.equals(Criterion.Type.ETH_DST_MASKED)) {
+                return matchMac(packet, (EthCriterion) criterion, true);
             } else {
                 return packet.criteria().contains(criterion);
             }
         });
     }
+
+    /**
+     * Checks if the packet has an dst or src IP and if that IP matches the subnet of the ip criterion.
+     *
+     * @param packet    the incoming packet
+     * @param criterion the criterion to match
+     * @return true if match
+     */
+    private boolean matchIp(TrafficSelector packet, IPCriterion criterion) {
+        IPCriterion matchCriterion = (IPCriterion) packet.getCriterion(criterion.type());
+        //if the packet does not have an IPv4 or IPv6 criterion we return true
+        if (matchCriterion == null) {
+            return false;
+        }
+        try {
+            log.debug("Checking if {} is under {}", matchCriterion.ip(), criterion.ip());
+            Subnet subnet = Subnet.createInstance(criterion.ip().toString());
+            return subnet.isInSubnet(matchCriterion.ip().address().toInetAddress());
+        } catch (UnknownHostException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Checks if the packet has a dst or src MAC and if that Mac matches the mask of the mac criterion.
+     *
+     * @param packet       the incoming packet
+     * @param hitCriterion the criterion to match
+     * @param dst          true if we are checking DST MAC
+     * @return true if match
+     */
+    private boolean matchMac(TrafficSelector packet, EthCriterion hitCriterion, boolean dst) {
+        //Packet can have only one EthCriterion
+        EthCriterion matchCriterion;
+        if (dst) {
+            matchCriterion = (EthCriterion) packet.criteria().stream().filter(criterion1 -> {
+                return criterion1.type().equals(Criterion.Type.ETH_DST_MASKED) ||
+                        criterion1.type().equals(Criterion.Type.ETH_DST);
+            }).findFirst().orElse(null);
+        } else {
+            matchCriterion = (EthCriterion) packet.criteria().stream().filter(criterion1 -> {
+                return criterion1.type().equals(Criterion.Type.ETH_SRC_MASKED) ||
+                        criterion1.type().equals(Criterion.Type.ETH_SRC);
+            }).findFirst().orElse(null);
+        }
+        //if the packet does not have an ETH criterion we return true
+        if (matchCriterion == null) {
+            return true;
+        }
+        log.debug("Checking if {} is under {}/{}", matchCriterion.mac(), hitCriterion.mac(), hitCriterion.mask());
+        return compareMac(matchCriterion.mac(), hitCriterion.mac(), hitCriterion.mask());
+    }
 }
diff --git a/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootUtils.java b/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootUtils.java
index f89a413..57758b3 100644
--- a/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootUtils.java
+++ b/apps/t3/src/main/java/org/onosproject/t3/impl/TroubleshootUtils.java
@@ -17,6 +17,7 @@
 package org.onosproject.t3.impl;
 
 import com.google.common.collect.ImmutableMap;
+import org.onlab.packet.MacAddress;
 
 import java.util.Map;
 
@@ -43,4 +44,36 @@
             .put("accton-ofdpa3", true)
             .put("znyx-ofdpa", true)
             .build();
+
+    /**
+     * Checks if the Mac Address is inside a range between the min MAC and the mask.
+     * @param macAddress the MAC address to check
+     * @param minAddr the min MAC address
+     * @param maskAddr the mask
+     * @return true if in range, false otherwise.
+     */
+    static boolean compareMac(MacAddress macAddress, MacAddress minAddr, MacAddress maskAddr) {
+        byte[] mac = macAddress.toBytes();
+        byte[] min = minAddr.toBytes();
+        byte[] mask = maskAddr.toBytes();
+        boolean inRange = true;
+
+        int i = 0;
+
+        //if mask is 00 stop
+        while (inRange && i < mask.length && (mask[i] & 0xFF) != 0) {
+            int ibmac = mac[i] & 0xFF;
+            int ibmin = min[i] & 0xFF;
+            int ibmask = mask[i] & 0xFF;
+            if (ibmask == 255) {
+                inRange = ibmac == ibmin;
+            } else if (ibmac < ibmin || ibmac >= ibmask) {
+                inRange = false;
+                break;
+            }
+            i++;
+        }
+
+        return inRange;
+    }
 }
diff --git a/apps/t3/src/test/java/org/onosproject/t3/impl/T3TestObjects.java b/apps/t3/src/test/java/org/onosproject/t3/impl/T3TestObjects.java
index 4e1eb68..0885653 100644
--- a/apps/t3/src/test/java/org/onosproject/t3/impl/T3TestObjects.java
+++ b/apps/t3/src/test/java/org/onosproject/t3/impl/T3TestObjects.java
@@ -524,6 +524,37 @@
             .build();
     static final FlowEntry DEFERRED_FLOW_ENTRY = new DefaultFlowEntry(DEFERRED_FLOW);
 
+    //Multicast Flow and Group Test
+    static final DeviceId MULTICAST_GROUP_FLOW_DEVICE = DeviceId.deviceId("MulticastGroupFlowDevice");
+
+    private static final TrafficSelector MULTICAST_FLOW_SELECTOR = DefaultTrafficSelector.builder()
+            .matchInPort(PortNumber.portNumber(1))
+            .matchIPDst(IpPrefix.valueOf("224.0.0.1/32"))
+            .matchEthDst(MacAddress.valueOf("01:00:5e:00:00:01"))
+            .build();
+
+    private static final FlowRule MULTICAST_GROUP_FLOW =
+            DefaultFlowEntry.builder().forDevice(MULTICAST_GROUP_FLOW_DEVICE)
+                    .forTable(0)
+                    .withPriority(100)
+                    .withSelector(MULTICAST_FLOW_SELECTOR)
+                    .withTreatment(GROUP_FLOW_TREATMENT)
+                    .fromApp(new DefaultApplicationId(0, "TestApp"))
+                    .makePermanent()
+                    .build();
+
+    static final FlowEntry MULTICAST_GROUP_FLOW_ENTRY = new DefaultFlowEntry(MULTICAST_GROUP_FLOW);
+
+    static final Group MULTICAST_GROUP = new DefaultGroup(GROUP_ID, MULTICAST_GROUP_FLOW_DEVICE,
+            Group.Type.SELECT, BUCKETS_MULTIPLE);
+
+    static final ConnectPoint MULTICAST_IN_CP = ConnectPoint.deviceConnectPoint(MULTICAST_GROUP_FLOW_DEVICE + "/" + 1);
+
+    static final ConnectPoint MULTICAST_OUT_CP = ConnectPoint.deviceConnectPoint(MULTICAST_GROUP_FLOW_DEVICE + "/" + 3);
+
+    static final ConnectPoint MULTICAST_OUT_CP_2 =
+            ConnectPoint.deviceConnectPoint(MULTICAST_GROUP_FLOW_DEVICE + "/" + 2);
+
     //match on port 1, clear deferred actions and output
     private static final TrafficTreatment DEFERRED_CLEAR_1_FLOW_TREATMENT = DefaultTrafficTreatment.builder()
             .wipeDeferred()
@@ -599,6 +630,13 @@
             .matchEthType(EthType.EtherType.LLDP.ethType().toShort())
             .build();
 
+    static final TrafficSelector PACKET_OK_MULTICAST = DefaultTrafficSelector.builder()
+            .matchInPort(PortNumber.portNumber(1))
+            .matchEthType(EthType.EtherType.IPV4.ethType().toShort())
+            .matchEthDst(MacAddress.valueOf("01:00:5e:00:00:01"))
+            .matchIPDst(IpPrefix.valueOf("224.0.0.1/32"))
+            .build();
+
     static final TrafficSelector PACKET_FAIL = DefaultTrafficSelector.builder()
             .matchInPort(PortNumber.portNumber(1))
             .matchIPSrc(IpPrefix.valueOf("127.0.0.1/32"))
diff --git a/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootManagerTest.java b/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootManagerTest.java
index 7b6c570..6a80c12 100644
--- a/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootManagerTest.java
+++ b/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootManagerTest.java
@@ -30,15 +30,18 @@
 import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.DefaultDevice;
 import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DefaultPort;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.Host;
 import org.onosproject.net.Link;
 import org.onosproject.net.PortNumber;
+import org.onosproject.net.Port;
 import org.onosproject.net.device.DeviceServiceAdapter;
 import org.onosproject.net.driver.DefaultDriver;
 import org.onosproject.net.driver.Driver;
 import org.onosproject.net.driver.DriverServiceAdapter;
+import org.onosproject.net.edge.EdgePortServiceAdapter;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRuleServiceAdapter;
 import org.onosproject.net.flow.TrafficSelector;
@@ -84,6 +87,7 @@
         mngr.groupService = new TestGroupService();
         mngr.deviceService = new TestDeviceService();
         mngr.mastershipService = new TestMastershipService();
+        mngr.edgePortService = new TestEdgePortService();
 
         assertNotNull("Manager should not be null", mngr);
 
@@ -320,6 +324,32 @@
         log.info("trace {}", traceSuccess.resultMessage());
     }
 
+    /**
+     * Test multicast in single device.
+     */
+    @Test
+    public void multicastTest() throws Exception {
+
+        StaticPacketTrace traceSuccess = mngr.trace(PACKET_OK_MULTICAST, MULTICAST_IN_CP);
+
+        log.info("trace {}", traceSuccess);
+
+        log.info("trace {}", traceSuccess.resultMessage());
+
+        assertNotNull("trace should not be null", traceSuccess);
+        assertEquals("Trace should have " + 2 + " output", 2,
+                traceSuccess.getGroupOuputs(MULTICAST_GROUP_FLOW_DEVICE).size());
+        assertEquals("Trace should only have " + 2 + "output", 2,
+                traceSuccess.getCompletePaths().size());
+        assertTrue("Trace should be successful",
+                traceSuccess.resultMessage().contains("reached output"));
+        assertEquals("Incorrect Output CP", MULTICAST_OUT_CP_2,
+                traceSuccess.getGroupOuputs(MULTICAST_GROUP_FLOW_DEVICE).get(0).getOutput());
+        assertEquals("Incorrect Output CP", MULTICAST_OUT_CP,
+                traceSuccess.getGroupOuputs(MULTICAST_GROUP_FLOW_DEVICE).get(1).getOutput());
+
+    }
+
     private StaticPacketTrace testSuccess(TrafficSelector packet, ConnectPoint in, DeviceId deviceId, ConnectPoint out,
                                           int paths, int outputs) {
         StaticPacketTrace traceSuccess = mngr.trace(packet, in);
@@ -384,6 +414,8 @@
                         HARDWARE_10_OUTPUT_FLOW_ENTRY);
             } else if (deviceId.equals(LLDP_FLOW_DEVICE)) {
                 return ImmutableList.of(LLDP_FLOW_ENTRY);
+            } else if (deviceId.equals(MULTICAST_GROUP_FLOW_DEVICE)) {
+                return ImmutableList.of(MULTICAST_GROUP_FLOW_ENTRY);
             }
             return ImmutableList.of();
         }
@@ -410,6 +442,8 @@
                 return ImmutableList.of(TOPO_GROUP);
             } else if (deviceId.equals(DUAL_LINK_1) || deviceId.equals(DUAL_LINK_2)) {
                 return ImmutableList.of(DUAL_LINK_GROUP);
+            } else if (deviceId.equals(MULTICAST_GROUP_FLOW_DEVICE)) {
+                return ImmutableList.of(MULTICAST_GROUP);
             }
             return ImmutableList.of();
         }
@@ -424,7 +458,16 @@
                     connectPoint.equals(DUAL_LINK_2_CP_2_OUT) || connectPoint.equals(DUAL_LINK_2_CP_3_OUT)) {
                 return ImmutableSet.of();
             }
-            return ImmutableSet.of(H1);
+            if (connectPoint.equals(SINGLE_FLOW_OUT_CP) ||
+                    connectPoint.equals(DUAL_FLOW_OUT_CP) ||
+                    connectPoint.equals(GROUP_FLOW_OUT_CP) ||
+                    connectPoint.equals(HARDWARE_DEVICE_OUT_CP) ||
+                    connectPoint.equals(HARDWARE_DEVICE_10_OUT_CP) ||
+                    connectPoint.equals(DEFERRED_CP_2_OUT) ||
+                    connectPoint.equals(DUAL_LINK_3_CP_3_OUT)) {
+                return ImmutableSet.of(H1);
+            }
+            return ImmutableSet.of();
         }
 
         @Override
@@ -525,11 +568,22 @@
         }
 
         @Override
+        public Port getPort(ConnectPoint cp) {
+            return new DefaultPort(null, cp.port(), true, DefaultAnnotations.builder().build());
+        }
+
+        @Override
         public boolean isAvailable(DeviceId deviceId) {
-            if (deviceId.equals(OFFLINE_DEVICE)) {
-                return false;
-            }
-            return true;
+            return !deviceId.equals(OFFLINE_DEVICE);
+        }
+    }
+
+    private class TestEdgePortService extends EdgePortServiceAdapter {
+
+        @Override
+        public boolean isEdgePoint(ConnectPoint point) {
+            return point.equals(MULTICAST_OUT_CP) ||
+                    point.equals(MULTICAST_OUT_CP_2);
         }
     }
 
diff --git a/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootUtilsTest.java b/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootUtilsTest.java
new file mode 100644
index 0000000..c8195c0
--- /dev/null
+++ b/apps/t3/src/test/java/org/onosproject/t3/impl/TroubleshootUtilsTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.t3.impl;
+
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test for util methods of the Trellis Troubleshoot Toolkit.
+ */
+public class TroubleshootUtilsTest {
+
+    @Test
+    public void testMacMatch() {
+
+        MacAddress min = MacAddress.valueOf("01:00:5E:00:00:00");
+        MacAddress mask = MacAddress.valueOf("FF:FF:FF:80:00:00");
+        MacAddress macOk = MacAddress.valueOf("01:00:5E:00:00:01");
+
+        assertTrue("False on correct match", TroubleshootUtils.compareMac(macOk, min, mask));
+
+        MacAddress macWrong = MacAddress.valueOf("01:00:5E:80:00:00");
+
+        assertFalse("True on false match", TroubleshootUtils.compareMac(macWrong, min, mask));
+
+        MacAddress maskEmpty = MacAddress.valueOf("00:00:00:00:00:00");
+
+        assertTrue("False on empty Mask", TroubleshootUtils.compareMac(macOk, min, maskEmpty));
+
+        MacAddress maskFull = MacAddress.valueOf("FF:FF:FF:FF:FF:FF");
+
+        assertFalse("True on full Mask", TroubleshootUtils.compareMac(macOk, min, maskFull));
+
+    }
+}
\ No newline at end of file