VOL-506: Multiple PON Multicast support. Original correction by Niren Chidrawar.

Change-Id: Ia41dbd8b88d007db96e7773bfc95b78481d9fa13
diff --git a/tests/utests/voltha/core/flow_helpers.py b/tests/utests/voltha/core/flow_helpers.py
index 856f986..2141f6f 100644
--- a/tests/utests/voltha/core/flow_helpers.py
+++ b/tests/utests/voltha/core/flow_helpers.py
@@ -37,3 +37,10 @@
         diff = make_patch(msg1_dict, msg2_dict)
         return dumps(diff.patch, indent=2)
 
+    def assertFlowNotInFlows(self, flow, flows):
+        if flow in flows.items:
+            self.fail('flow id %d is in flows' % flow.id)
+
+    def assertFlowInFlows(self, flow, flows):
+        if flow not in flows.items:
+            self.fail('flow id %d is not in flows' % flow.id)
diff --git a/tests/utests/voltha/core/test_logical_device_agent.py b/tests/utests/voltha/core/test_logical_device_agent.py
index bfa6c5b..d9d7652 100644
--- a/tests/utests/voltha/core/test_logical_device_agent.py
+++ b/tests/utests/voltha/core/test_logical_device_agent.py
@@ -640,7 +640,7 @@
             actions=[group(2)]
         ))
         self.lda._flow_table_updated(self.flows)
-        self.assertEqual(len(self.device_flows['olt'].items), 1)
+        self.assertEqual(len(self.device_flows['olt'].items), 0)
         self.assertEqual(len(self.device_flows['onu1'].items), 3)
         self.assertEqual(len(self.device_flows['onu2'].items), 3)
         self.assertEqual(len(self.device_flows['onu2'].items), 3)
@@ -648,7 +648,7 @@
         self.assertEqual(len(self.device_groups['onu1'].items), 0)
         self.assertEqual(len(self.device_groups['onu2'].items), 0)
 
-        self.assertFlowsEqual(self.device_flows['olt'].items[0], mk_flow_stat(
+        self.assertFlowNotInFlows(mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0),  eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe60a0a0a)],
@@ -656,7 +656,7 @@
                 pop_vlan(),
                 output(1)
             ]
-        ))
+        ), self.device_flows['olt'])
 
     # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ COMPLEX TESTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -769,7 +769,7 @@
         self.lda._flow_table_updated(self.flows)
 
         # now check device level flows
-        self.assertEqual(len(self.device_flows['olt'].items), 16)
+        self.assertEqual(len(self.device_flows['olt'].items), 15)
         self.assertEqual(len(self.device_flows['onu1'].items), 5)
         self.assertEqual(len(self.device_flows['onu2'].items), 5)
         self.assertEqual(len(self.device_groups['olt'].items), 0)
@@ -835,37 +835,37 @@
             actions=[push_vlan(0x8100), set_field(vlan_vid(4096 + 4000)),
                      output(0)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[10], mk_flow_stat(
+        self.assertFlowNotInFlows(mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe4010101)],
             actions=[pop_vlan(), output(1)]
-        ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[11], mk_flow_stat(
+        ), self.device_flows['olt'])
+        self.assertFlowsEqual(self.device_flows['olt'].items[10], mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe4010102)],
             actions=[pop_vlan(), output(1)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[12], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[11], mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe4010103)],
             actions=[pop_vlan(), output(1)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[13], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[12], mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe4010104)],
             actions=[pop_vlan(), output(1)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[14], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[13], mk_flow_stat(
             priority=500,
             match_fields=[in_port(1), vlan_vid(4096 + 101)],
             actions=[
                 push_vlan(0x8100), set_field(vlan_vid(4096 + 1000)), output(0)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[15], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[14], mk_flow_stat(
             priority=500,
             match_fields=[in_port(1), vlan_vid(4096 + 102)],
             actions=[
diff --git a/tests/utests/voltha/core/test_multipon_lda.py b/tests/utests/voltha/core/test_multipon_lda.py
index cb99cf2..31a8d9d 100644
--- a/tests/utests/voltha/core/test_multipon_lda.py
+++ b/tests/utests/voltha/core/test_multipon_lda.py
@@ -334,7 +334,7 @@
                           ipv4_dst(0xe60a0a0a)],
             actions=[
                 pop_vlan(),
-                output(2)
+                output(1)
             ]
         ))
         self.assertFlowsEqual(self.device_flows['onu2'].items[3], mk_flow_stat(
@@ -361,7 +361,7 @@
             actions=[group(2)]
         ))
         self.lda._flow_table_updated(self.flows)
-        self.assertEqual(len(self.device_flows['olt'].items), 1)
+        self.assertEqual(len(self.device_flows['olt'].items), 0)
         self.assertEqual(len(self.device_flows['onu1'].items), 3)
         self.assertEqual(len(self.device_flows['onu2'].items), 3)
         self.assertEqual(len(self.device_flows['onu2'].items), 3)
@@ -369,7 +369,7 @@
         self.assertEqual(len(self.device_groups['onu1'].items), 0)
         self.assertEqual(len(self.device_groups['onu2'].items), 0)
 
-        self.assertFlowsEqual(self.device_flows['olt'].items[0], mk_flow_stat(
+        self.assertFlowNotInFlows(mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe60a0a0a)],
@@ -377,7 +377,7 @@
                 pop_vlan(),
                 output(2)
             ]
-        ))
+        ), self.device_flows['olt'])
     # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ COMPLEX TESTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
     def test_complex_flow_table_decomposition(self):
@@ -489,7 +489,7 @@
         self.lda._flow_table_updated(self.flows)
 
         # now check device level flows
-        self.assertEqual(len(self.device_flows['olt'].items), 18)
+        self.assertEqual(len(self.device_flows['olt'].items), 17)
         self.assertEqual(len(self.device_flows['onu1'].items), 5)
         self.assertEqual(len(self.device_flows['onu2'].items), 5)
         self.assertEqual(len(self.device_groups['olt'].items), 0)
@@ -555,47 +555,47 @@
             actions=[push_vlan(0x8100), set_field(vlan_vid(4096 + 4000)),
                      output(0)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[10], mk_flow_stat(
+        self.assertFlowNotInFlows(mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
                           ipv4_dst(0xe4010101)],
             actions=[pop_vlan(), output(2)]
+        ), self.device_flows['olt'])
+        self.assertFlowsEqual(self.device_flows['olt'].items[10], mk_flow_stat(
+            priority=1000,
+            match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
+                          ipv4_dst(0xe4010102)],
+            actions=[pop_vlan(), output(1)]
         ))
         self.assertFlowsEqual(self.device_flows['olt'].items[11], mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
-                          ipv4_dst(0xe4010102)],
-            actions=[pop_vlan(), output(2)]
+                          ipv4_dst(0xe4010103)],
+            actions=[pop_vlan(), output(1)]
         ))
         self.assertFlowsEqual(self.device_flows['olt'].items[12], mk_flow_stat(
             priority=1000,
             match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
-                          ipv4_dst(0xe4010103)],
-            actions=[pop_vlan(), output(2)]
+                          ipv4_dst(0xe4010104)],
+            actions=[pop_vlan(), output(1)]
         ))
         self.assertFlowsEqual(self.device_flows['olt'].items[13], mk_flow_stat(
-            priority=1000,
-            match_fields=[in_port(0), eth_type(0x800), vlan_vid(4096 + 140),
-                          ipv4_dst(0xe4010104)],
-            actions=[pop_vlan(), output(2)]
-        ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[14], mk_flow_stat(
             priority=500,
             match_fields=[in_port(0), metadata(101), vlan_vid(4096 + 1000)],
             actions=[pop_vlan(), output(1)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[15], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[14], mk_flow_stat(
             priority=500,
             match_fields=[in_port(1), vlan_vid(4096 + 101)],
             actions=[
                 push_vlan(0x8100), set_field(vlan_vid(4096 + 1000)), output(0)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[16], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[15], mk_flow_stat(
             priority=500,
             match_fields=[in_port(0), metadata(201), vlan_vid(4096 + 1000)],
             actions=[pop_vlan(), output(2)]
         ))
-        self.assertFlowsEqual(self.device_flows['olt'].items[17], mk_flow_stat(
+        self.assertFlowsEqual(self.device_flows['olt'].items[16], mk_flow_stat(
             priority=500,
             match_fields=[in_port(2), vlan_vid(4096 + 201)],
             actions=[
diff --git a/voltha/core/flow_decomposer.py b/voltha/core/flow_decomposer.py
index fad56dc..d5bb69f 100644
--- a/voltha/core/flow_decomposer.py
+++ b/voltha/core/flow_decomposer.py
@@ -454,14 +454,19 @@
 def hash_flow_stats(flow):
     """
     Return unique 64-bit integer hash for flow covering the following
-    attributes: 'table_id', 'priority', 'flags', 'cookie', 'match'
+    attributes: 'table_id', 'priority', 'flags', 'cookie', 'match', '_instruction_string'
     """
-    hex = md5('{},{},{},{},{}'.format(
+    _instruction_string = ""
+    for _instruction in flow.instructions:
+        _instruction_string += _instruction.SerializeToString()
+
+    hex = md5('{},{},{},{},{},{}'.format(
         flow.table_id,
         flow.priority,
         flow.flags,
         flow.cookie,
-        flow.match.SerializeToString()
+        flow.match.SerializeToString(),
+        _instruction_string
     )).hexdigest()
     return int(hex[:16], 16)
 
@@ -522,7 +527,9 @@
             self._egress_port == other._egress_port)
     def __ne__(self, other):
         return not self.__eq__(other)
-
+    def __str__(self):
+        return 'RouteHop device_id {}, ingress_port {}, egress_port {}'.format(
+            self._device.id, self._ingress_port, self._egress_port)
 
 class FlowDecomposer(object):
 
@@ -781,73 +788,85 @@
 
                     ))
 
-                else:  # multicast case
+                else:
                     grp_id = get_group(flow)
-                    assert grp_id is not None
+                    
+                    if grp_id is not None: # multicast case
 
-                    fl_lst, _ = device_rules.setdefault(
-                        ingress_hop.device.id, ([], []))
-                    fl_lst.append(mk_flow_stat(
-                        priority=flow.priority,
-                        cookie=flow.cookie,
-                        match_fields=[
-                            in_port(ingress_hop.ingress_port.port_no)
-                        ] + [
-                            field for field in get_ofb_fields(flow)
-                            if field.type not in (IN_PORT,)
-                        ],
-                        actions=[
-                            action for action in get_actions(flow)
-                            if action.type not in (GROUP,)
-                        ] + [
-                            pop_vlan(),
-                            output(ingress_hop.egress_port.port_no)
-                        ]
-                    ))
+                        fl_lst_olt, _ = device_rules.setdefault(
+                            ingress_hop.device.id, ([], []))
 
-                    # having no group yet is the same as having a group with
-                    # no buckets
-                    group = group_map.get(grp_id, ofp.ofp_group_entry())
+                        # having no group yet is the same as having a group with
+                        # no buckets
+                        group = group_map.get(grp_id, ofp.ofp_group_entry())
 
-                    for bucket in group.desc.buckets:
-                        found_pop_vlan = False
-                        other_actions = []
-                        for action in bucket.actions:
-                            if action.type == POP_VLAN:
-                                found_pop_vlan = True
-                            elif action.type == OUTPUT:
-                                out_port_no = action.output.port
-                            else:
-                                other_actions.append(action)
-                        # re-run route request to determine egress device and
-                        # ports
-                        route2 = self.get_route(in_port_no, out_port_no)
-                        if route2 is None:
-                            log.error('mc-no-route', in_port_no=in_port_no,
-                                out_port_no=out_port_no, route2=route2,
-                                comment='deleting flow')
-                            self.flow_delete(flow)
-                            continue
+                        for bucket in group.desc.buckets:
+                            found_pop_vlan = False
+                            other_actions = []
+                            for action in bucket.actions:
+                                if action.type == POP_VLAN:
+                                    found_pop_vlan = True
+                                elif action.type == OUTPUT:
+                                    out_port_no = action.output.port
+                                else:
+                                    other_actions.append(action)
+                            # re-run route request to determine egress device and
+                            # ports
+                            route2 = self.get_route(in_port_no, out_port_no)
+                            if not route2 or len(route2) != 2:
+                                log.error('mc-no-route', in_port_no=in_port_no,
+                                    out_port_no=out_port_no, route2=route2,
+                                    comment='deleting flow')
+                                self.flow_delete(flow)
+                                continue
 
-                        assert len(route2) == 2
-                        ingress_hop2, egress_hop = route2
-                        assert ingress_hop.ingress_port == ingress_hop2.ingress_port
+                            ingress_hop2, egress_hop = route2
 
-                        fl_lst, _ = device_rules.setdefault(
-                            egress_hop.device.id, ([], []))
-                        fl_lst.append(mk_flow_stat(
-                            priority=flow.priority,
-                            cookie=flow.cookie,
-                            match_fields=[
-                                in_port(egress_hop.ingress_port.port_no)
-                            ] + [
-                                field for field in get_ofb_fields(flow)
-                                if field.type not in (IN_PORT, VLAN_VID, VLAN_PCP)
-                            ],
-                            actions=other_actions + [
-                                output(egress_hop.egress_port.port_no)
-                            ]
-                        ))
+                            if ingress_hop.ingress_port != ingress_hop2.ingress_port:
+                                log.error('mc-ingress-hop-hop2-mismatch',
+                                    ingress_hop=ingress_hop,
+                                    ingress_hop2=ingress_hop2,
+                                    in_port_no=in_port_no,
+                                    out_port_no=out_port_no,
+                                    comment='ignoring flow')
+                                continue
+
+                            fl_lst_olt.append(mk_flow_stat(
+                                priority=flow.priority,
+                                cookie=flow.cookie,
+                                match_fields=[
+                                    in_port(ingress_hop.ingress_port.port_no)
+                                ] + [
+                                    field for field in get_ofb_fields(flow)
+                                    if field.type not in (IN_PORT,)
+                                ],
+                                actions=[
+                                    action for action in get_actions(flow)
+                                    if action.type not in (GROUP,)
+                                ] + [
+                                    pop_vlan(),
+                                    output(egress_hop.ingress_port.port_no)
+                                ]
+                            ))
+
+                            fl_lst_onu, _ = device_rules.setdefault(
+                                egress_hop.device.id, ([], []))
+                            fl_lst_onu.append(mk_flow_stat(
+                                priority=flow.priority,
+                                cookie=flow.cookie,
+                                match_fields=[
+                                    in_port(egress_hop.ingress_port.port_no)
+                                ] + [
+                                    field for field in get_ofb_fields(flow)
+                                    if field.type not in (IN_PORT, VLAN_VID, VLAN_PCP)
+                                ],
+                                actions=other_actions + [
+                                    output(egress_hop.egress_port.port_no)
+                                ]
+                            ))
+                    else:
+                        raise NotImplementedError('undefined downstream case for flows')
+                        
 
         return device_rules