Complete IGMPv3 membership report

Change-Id: I900478972675ee60928b39bbdc55858d3bea998b
diff --git a/IGMP.py b/IGMP.py
index a747d09..1b24de3 100644
--- a/IGMP.py
+++ b/IGMP.py
@@ -25,6 +25,36 @@
 IGMP_SRC_MAC = "5a:e1:ac:ec:4d:a1"
 """
 
+
+class IGMPv3gr(Packet):
+    """IGMPv3 Group Record, used in membership report"""
+
+    name = "IGMPv3gr"
+
+    igmp_v3_gr_types = {
+        IGMP_V3_GR_TYPE_INCLUDE: "Include Mode",
+        IGMP_V3_GR_TYPE_EXCLUDE: "Exclude Mode",
+        IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE: "Change to Include Mode",
+        IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE: "Change to Exclude Mode",
+        IGMP_V3_GR_TYPE_ALLOW_NEW: "Allow New Sources",
+        IGMP_V3_GR_TYPE_BLOCK_OLD: "Block Old Sources"
+    }
+
+    fields_desc = [
+        ByteEnumField("rtype", IGMP_V3_GR_TYPE_INCLUDE, igmp_v3_gr_types),
+        ByteField("aux_data_len", 0),
+        FieldLenField("numsrc", None, count_of="sources"),
+        IPField("mcaddr", "0.0.0.0"),
+        FieldListField("sources", None, IPField("src", "0.0.0.0"), "numsrc")
+    ]
+
+    def post_build(self, pkt, payload):
+        pkt += payload
+        if self.aux_data_len != 0:
+            print "WARNING: Auxiliary Data Length must be zero (0)"
+        return pkt
+
+
 class IGMPv3(Packet):
 
     name = "IGMPv3"
@@ -50,10 +80,14 @@
         ConditionalField(BitField("qrv", 0, 3), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
         ConditionalField(ByteField("qqic", 0), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
         ConditionalField(FieldLenField("numsrc", None, count_of="srcs"), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
-        ConditionalField(FieldListField("srcs", None, IPField("src", "0.0.0.0"), "numsrc"), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY)
+        ConditionalField(FieldListField("srcs", None, IPField("src", "0.0.0.0"), "numsrc"), lambda pkt: pkt.type == IGMP_TYPE_MEMBERSHIP_QUERY),
 
         # membership report fields
-        # TODO
+        ConditionalField(ShortField("resv2", 0), lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT),
+        ConditionalField(FieldLenField("numgrp", None, count_of="grps"), lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT),
+        ConditionalField(PacketListField("grps", [], IGMPv3gr), lambda pkt: pkt.type == IGMP_TYPE_V3_MEMBERSHIP_REPORT)
+
+        # TODO: v2 and v3 membership reports?
 
     ]
 
@@ -138,34 +172,6 @@
         return pkt
 
 
-class IGMPv3gr(Packet):
-
-    name = "IGMPv3gr"
-
-    igmp_v3_gr_types = {
-        IGMP_V3_GR_TYPE_INCLUDE: "Include Mode",
-        IGMP_V3_GR_TYPE_EXCLUDE: "Exclude Mode",
-        IGMP_V3_GR_TYPE_CHANGE_TO_INCLUDE: "Change to Include Mode",
-        IGMP_V3_GR_TYPE_CHANGE_TO_EXCLUDE: "Change to Exclude Mode",
-        IGMP_V3_GR_TYPE_ALLOW_NEW: "Allow New Sources",
-        IGMP_V3_GR_TYPE_BLOCK_OLD: "Block Old Sources"
-    }
-
-    fields_desc = [
-        ByteEnumField("rtype", IGMP_V3_GR_TYPE_INCLUDE, igmp_v3_gr_types),
-        ByteField("aux_data_len", 0),
-        FieldLenField("numsrc", None, "sources"),
-        IPField("mcast_addr", "0.0.0.0"),
-        FieldListField("sources", None, IPField("src", "0.0.0.0"), "numsrc")
-    ]
-
-    def post_build(self, pkt, payload):
-        pkt += payload
-        if self.aux_data_len != 0:
-            print "WARNING: Auxiliary Data Length must be zero (0)"
-        return pkt
-
-
 bind_layers(IP,       IGMPv3,   frag=0, proto=2, ttl=1, tos=0xc0)
 bind_layers(IGMPv3,   IGMPv3gr, frag=0, proto=2)
 bind_layers(IGMPv3gr, IGMPv3gr, frag=0, proto=2)
@@ -207,4 +213,22 @@
     IGMPv3.fixup(pkt)
     hexdump(str(pkt))
 
+    print "construct v3 membership report - join a single group"
+    mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1")
+    mr.grps = [IGMPv3gr( rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30")]
+    hexdump(mr)
+
+    print "construct v3 membership report - join two groups"
+    mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1")
+    mr.grps = [
+        IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.30"),
+        IGMPv3gr(rtype=IGMP_V3_GR_TYPE_EXCLUDE, mcaddr="229.10.20.31")
+    ]
+    hexdump(mr)
+
+    print "construct v3 membership report - leave a group"
+    mr = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1")
+    mr.grps = [IGMPv3gr(rtype=IGMP_V3_GR_TYPE_INCLUDE, mcaddr="229.10.20.30")]
+    hexdump(mr)
+
     print "all ok"
diff --git a/olt.py b/olt.py
index a8e26c4..4f78f2a 100644
--- a/olt.py
+++ b/olt.py
@@ -14,7 +14,8 @@
 
 from oftest.testutils import *
 
-from IGMP import IGMPv3
+from IGMP import IGMPv3, IGMPv3gr, IGMP_TYPE_V3_MEMBERSHIP_REPORT, IGMP_V3_GR_TYPE_INCLUDE
+
 
 # These parameters can be altered from the command line using the -t or --test-params= options.
 # Example: -t 'onu_port=129;olt_port=288;device_type=pmc'
@@ -171,9 +172,9 @@
         match.oxm_list.append(ofp.oxm.eth_type(0x800))
         match.oxm_list.append(ofp.oxm.ip_proto(2))
 
-        pkt = scapy.Ether(dst='01:00:5E:7F:FF:FF', src='00:00:00:00:00:01')/ \
-              scapy.IP(src='10.0.0.1', dst='10.0.0.2', ttl=60, tos=0, id=0, proto=2)
-
+        igmp = IGMPv3(type=IGMP_TYPE_V3_MEMBERSHIP_REPORT, max_resp_code=30, gaddr="224.0.0.1")
+        igmp.grps = [IGMPv3gr(rtype=IGMP_V3_GR_TYPE_INCLUDE, mcaddr="229.10.20.30")]
+        pkt = IGMPv3.fixup( scapy.Ether(src='00:00:00:00:be:ef') / scapy.IP() / igmp )
         pkt = pkt / ("0" * (100 - len(pkt)))
 
         testPacketIn(self, match, pkt)