[SEBA-660] : Adding Igmp support in BbSim

Change-Id: I9f5c7d8ad39ac82850b04e2c997996d6c47b32d2
diff --git a/internal/bbsim/api/onus_handler.go b/internal/bbsim/api/onus_handler.go
index 7b7d6ce..cec8803 100644
--- a/internal/bbsim/api/onus_handler.go
+++ b/internal/bbsim/api/onus_handler.go
@@ -160,6 +160,46 @@
 	return res, nil
 }
 
+func (s BBSimServer) ChangeIgmpState(ctx context.Context, req *bbsim.IgmpRequest) (*bbsim.Response, error) {
+	res := &bbsim.Response{}
+
+	logger.WithFields(log.Fields{
+		"OnuSn":     req.OnuReq.SerialNumber,
+		"subAction": req.SubActionVal,
+	}).Infof("Received igmp request for ONU")
+
+	olt := devices.GetOLT()
+	onu, err := olt.FindOnuBySn(req.OnuReq.SerialNumber)
+
+	if err != nil {
+		res.StatusCode = int32(codes.NotFound)
+		res.Message = err.Error()
+		fmt.Println("ONU not found for sending igmp packet.")
+		return res, err
+	} else {
+		event := ""
+		switch req.SubActionVal {
+		case bbsim.SubActionTypes_JOIN:
+			event = "igmp_join_start"
+		case bbsim.SubActionTypes_LEAVE:
+			event = "igmp_leave"
+		}
+
+		if igmpErr := onu.InternalState.Event(event); igmpErr != nil {
+			logger.WithFields(log.Fields{
+				"OnuId":  onu.ID,
+				"IntfId": onu.PonPortID,
+				"OnuSn":  onu.Sn(),
+			}).Errorf("IGMP request failed: %s", igmpErr.Error())
+			res.StatusCode = int32(codes.FailedPrecondition)
+			res.Message = err.Error()
+			return res, igmpErr
+		}
+	}
+
+	return res, nil
+}
+
 func (s BBSimServer) RestartEapol(ctx context.Context, req *bbsim.ONURequest) (*bbsim.Response, error) {
 	res := &bbsim.Response{}
 
diff --git a/internal/bbsim/devices/messageTypes.go b/internal/bbsim/devices/messageTypes.go
index fd1d4dc..94c0731 100644
--- a/internal/bbsim/devices/messageTypes.go
+++ b/internal/bbsim/devices/messageTypes.go
@@ -43,7 +43,11 @@
 	SendDhcpFlow   MessageType = 13
 	OnuPacketIn    MessageType = 14
 
-	AlarmIndication MessageType = 15 // message data is an openolt.AlarmIndication
+	//IGMP
+	IGMPMembershipReportV2 MessageType = 15 // Version 2 Membership Report (JOIN)
+	IGMPLeaveGroup         MessageType = 16 // Leave Group
+
+	AlarmIndication MessageType = 17 // message data is an openolt.AlarmIndication
 )
 
 func (m MessageType) String() string {
@@ -63,6 +67,8 @@
 		"SendEapolFlow",
 		"SendDhcpFlow",
 		"OnuPacketIn",
+		"IGMPMembershipReportV2",
+		"IGMPLeaveGroup",
 	}
 	return names[m]
 }
diff --git a/internal/bbsim/devices/onu.go b/internal/bbsim/devices/onu.go
index 2ec674c..c9c2f17 100644
--- a/internal/bbsim/devices/onu.go
+++ b/internal/bbsim/devices/onu.go
@@ -30,6 +30,7 @@
 	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
 	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
 	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
+	"github.com/opencord/bbsim/internal/bbsim/responders/igmp"
 	"github.com/opencord/bbsim/internal/common"
 	omcilib "github.com/opencord/bbsim/internal/common/omci"
 	omcisim "github.com/opencord/omci-sim"
@@ -138,6 +139,11 @@
 			// TODO add start OMCI state
 			{Name: "send_eapol_flow", Src: []string{"initialized"}, Dst: "eapol_flow_sent"},
 			{Name: "send_dhcp_flow", Src: []string{"eapol_flow_sent"}, Dst: "dhcp_flow_sent"},
+			// IGMP
+			{Name: "igmp_join_start", Src: []string{"eap_response_success_received", "gem_port_added", "eapol_flow_received"}, Dst: "igmp_join_start"},
+			{Name: "igmp_join_done", Src: []string{"igmp_join_start"}, Dst: "igmp_join_done"},
+			{Name: "igmp_join_error", Src: []string{"igmp_join_start"}, Dst: "igmp_join_error"},
+			{Name: "igmp_leave", Src: []string{"igmp_join_start"}, Dst: "igmp_left"},
 		},
 		fsm.Callbacks{
 			"enter_state": func(e *fsm.Event) {
@@ -237,6 +243,17 @@
 				}
 				o.Channel <- msg
 			},
+			"igmp_join_start": func(e *fsm.Event) {
+				msg := Message{
+					Type: IGMPMembershipReportV2,
+				}
+				o.Channel <- msg
+			},
+			"igmp_leave": func(e *fsm.Event) {
+				msg := Message{
+					Type: IGMPLeaveGroup}
+				o.Channel <- msg
+			},
 		},
 	)
 
@@ -348,6 +365,12 @@
 				o.sendEapolFlow(client)
 			case SendDhcpFlow:
 				o.sendDhcpFlow(client)
+			case IGMPMembershipReportV2:
+				log.Infof("Recieved IGMPMembershipReportV2 message on ONU channel")
+				igmp.SendIGMPMembershipReportV2(o.PonPortID, o.ID, o.Sn(), o.PortNo, o.HwAddress, stream)
+			case IGMPLeaveGroup:
+				log.Infof("Recieved IGMPLeaveGroupV2 message on ONU channel")
+				igmp.SendIGMPLeaveGroupV2(o.PonPortID, o.ID, o.Sn(), o.PortNo, o.HwAddress, stream)
 			default:
 				onuLogger.Warnf("Received unknown message data %v for type %v in OLT Channel", message.Data, message.Type)
 			}
diff --git a/internal/bbsim/responders/igmp/igmp.go b/internal/bbsim/responders/igmp/igmp.go
new file mode 100644
index 0000000..cfdb079
--- /dev/null
+++ b/internal/bbsim/responders/igmp/igmp.go
@@ -0,0 +1,250 @@
+/*
+ * 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 igmp
+
+import (
+	"encoding/binary"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
+	omci "github.com/opencord/omci-sim"
+	"github.com/opencord/voltha-protos/v2/go/openolt"
+	log "github.com/sirupsen/logrus"
+	"net"
+	"time"
+)
+
+func SendIGMPLeaveGroupV2(ponPortId uint32, onuId uint32, serialNumber string, portNo uint32, macAddress net.HardwareAddr, stream bbsim.Stream) error {
+	log.WithFields(log.Fields{
+		"OnuId":        onuId,
+		"SerialNumber": serialNumber,
+		"PortNo":       portNo,
+	}).Debugf("Entered SendIGMPLeaveGroupV2")
+	igmp := createIGMPV2LeaveRequestPacket()
+	pkt, err := serializeIgmpPacket(ponPortId, onuId, macAddress, igmp)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"OnuId":        onuId,
+			"IntfId":       ponPortId,
+			"SerialNumber": serialNumber,
+		}).Errorf("Seriliazation of igmp packet failed :  %s", err)
+		return err
+	}
+
+	gemid, err := omci.GetGemPortId(ponPortId, onuId)
+	if err != nil {
+		log.WithFields(log.Fields{
+			"OnuId":        onuId,
+			"IntfId":       ponPortId,
+			"SerialNumber": serialNumber,
+		}).Errorf("Can't retrieve GemPortId for IGMP: %s", err)
+		return err
+	}
+
+	data := &openolt.Indication_PktInd{
+		PktInd: &openolt.PacketIndication{
+			IntfType:  "pon",
+			IntfId:    ponPortId,
+			GemportId: uint32(gemid),
+			Pkt:       pkt,
+			PortNo:    portNo,
+		},
+	}
+	//Sending IGMP packets
+	if err := stream.Send(&openolt.Indication{Data: data}); err != nil {
+		log.Errorf("Fail to send IGMP PktInd indication for ONU: %s, IntfId: %s, SerialNumber: %s,  error: %v", onuId, ponPortId, serialNumber, err)
+		return err
+	}
+	return nil
+}
+
+func SendIGMPMembershipReportV2(ponPortId uint32, onuId uint32, serialNumber string, portNo uint32, macAddress net.HardwareAddr, stream bbsim.Stream) error {
+	log.WithFields(log.Fields{
+		"OnuId":        onuId,
+		"SerialNumber": serialNumber,
+		"PortNo":       portNo,
+	}).Debugf("Entered SendIGMPMembershipReportV2")
+	igmp := createIGMPV2MembershipReportPacket()
+	pkt, err := serializeIgmpPacket(ponPortId, onuId, macAddress, igmp)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"OnuId":        onuId,
+			"IntfId":       ponPortId,
+			"SerialNumber": serialNumber,
+		}).Errorf("Seriliazation of igmp packet failed :  %s", err)
+		return err
+	}
+
+	gemid, err := omci.GetGemPortId(ponPortId, onuId)
+	if err != nil {
+		log.WithFields(log.Fields{
+			"OnuId":        onuId,
+			"IntfId":       ponPortId,
+			"SerialNumber": serialNumber,
+		}).Errorf("Can't retrieve GemPortId for IGMP: %s", err)
+		return err
+	}
+
+	data := &openolt.Indication_PktInd{
+		PktInd: &openolt.PacketIndication{
+			IntfType:  "pon",
+			IntfId:    ponPortId,
+			GemportId: uint32(gemid),
+			Pkt:       pkt,
+			PortNo:    portNo,
+		},
+	}
+	//Sending IGMP packets
+	if err := stream.Send(&openolt.Indication{Data: data}); err != nil {
+		log.Errorf("Fail to send IGMP PktInd indication for ONU: %s, IntfId: %s, SerialNumber: %s,  error: %v", onuId, ponPortId, serialNumber, err)
+		return err
+	}
+	return nil
+}
+
+//func serializeIgmpPacket(intfId uint32, onuId uint32, srcMac net.HardwareAddr, igmp *layers.IGMP) ([]byte, error) {
+func createIGMPV2MembershipReportPacket() IGMP {
+	return IGMP{
+		Type:            0x16, //IGMPV2 Membership Report
+		MaxResponseTime: time.Duration(1),
+		Checksum:        0,
+		GroupAddress:    net.IPv4(224, 0, 0, 22),
+		Version:         2,
+	}
+}
+
+func createIGMPV2LeaveRequestPacket() IGMP {
+	return IGMP{
+		Type:            0x17, //IGMPV2 Leave Group
+		MaxResponseTime: time.Duration(1),
+		Checksum:        0,
+		GroupAddress:    net.IPv4(224, 0, 0, 22),
+		Version:         2,
+	}
+}
+
+func serializeIgmpPacket(intfId uint32, onuId uint32, srcMac net.HardwareAddr, igmp IGMP) ([]byte, error) {
+	buffer := gopacket.NewSerializeBuffer()
+	options := gopacket.SerializeOptions{
+		ComputeChecksums: true,
+		FixLengths:       true,
+	}
+
+	ethernetLayer := &layers.Ethernet{
+		SrcMAC:       srcMac,
+		DstMAC:       net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
+		EthernetType: layers.EthernetTypeIPv4,
+	}
+
+	ipLayer := &layers.IPv4{
+		Version:  4,
+		TOS:      0x10,
+		Id:       0,
+		TTL:      128,
+		SrcIP:    []byte{0, 0, 0, 0},
+		DstIP:    []byte{224, 0, 0, 22},
+		Protocol: layers.IPProtocolIGMP,
+		Options:  []layers.IPv4Option{{OptionType: 148, OptionLength: 4, OptionData: make([]byte, 0)}}, //Adding router alert option
+	}
+
+	if err := gopacket.SerializeLayers(buffer, options, ethernetLayer, ipLayer, igmp); err != nil {
+		return nil, err
+	}
+
+	return buffer.Bytes(), nil
+}
+
+//-----------------------------------------***********************---------------------------------
+// BaseLayer is a convenience struct which implements the LayerData and
+// LayerPayload functions of the Layer interface.
+type BaseLayer struct {
+	// Contents is the set of bytes that make up this layer.  IE: for an
+	// Ethernet packet, this would be the set of bytes making up the
+	// Ethernet frame.
+	Contents []byte
+	// Payload is the set of bytes contained by (but not part of) this
+	// Layer.  Again, to take Ethernet as an example, this would be the
+	// set of bytes encapsulated by the Ethernet protocol.
+	Payload []byte
+}
+
+type IGMPType uint8
+
+type IGMP struct {
+	BaseLayer
+	Type                    IGMPType
+	MaxResponseTime         time.Duration
+	Checksum                uint16
+	GroupAddress            net.IP
+	SupressRouterProcessing bool
+	RobustnessValue         uint8
+	IntervalTime            time.Duration
+	SourceAddresses         []net.IP
+	NumberOfGroupRecords    uint16
+	NumberOfSources         uint16
+	Version                 uint8 // IGMP protocol version
+}
+
+// SerializeTo writes the serialized form of this layer into the
+// SerializationBuffer, implementing gopacket.SerializableLayer.
+// See the docs for gopacket.SerializableLayer for more info.
+// SerializeTo writes the serialized form of this layer into the
+// SerializationBuffer, implementing gopacket.SerializableLayer.
+// See the docs for gopacket.SerializableLayer for more info.
+func (igmp IGMP) SerializeTo(b gopacket.SerializeBuffer, opts gopacket.SerializeOptions) error {
+	//	func (igmp *IGMP) SerializeTo(b gopacket.SerializeBuffer, opts gopacket.SerializeOptions) error {
+	log.Debugf("Serializing IGMP Packet")
+	//TODO - add  length check here
+	data, err := b.PrependBytes(8915)
+	if err != nil {
+		return err
+	}
+
+	data[0] = byte(igmp.Type)
+	data[1] = byte(igmp.MaxResponseTime)
+	data[2] = 0
+	data[3] = 0
+	copy(data[4:8], igmp.GroupAddress.To4())
+	if opts.ComputeChecksums {
+		igmp.Checksum = tcpipChecksum(data, 0)
+		binary.BigEndian.PutUint16(data[2:4], igmp.Checksum)
+	}
+	return nil
+}
+
+// Calculate the TCP/IP checksum defined in rfc1071.  The passed-in csum is any
+// initial checksum data that's already been computed.
+func tcpipChecksum(data []byte, csum uint32) uint16 {
+	// to handle odd lengths, we loop to length - 1, incrementing by 2, then
+	// handle the last byte specifically by checking against the original
+	// length.
+	length := len(data) - 1
+	for i := 0; i < length; i += 2 {
+		// For our test packet, doing this manually is about 25% faster
+		// (740 ns vs. 1000ns) than doing it by calling binary.BigEndian.Uint16.
+		csum += uint32(data[i]) << 8
+		csum += uint32(data[i+1])
+	}
+	if len(data)%2 == 1 {
+		csum += uint32(data[length]) << 8
+	}
+	for csum > 0xffff {
+		csum = (csum >> 16) + (csum & 0xffff)
+	}
+	return ^uint16(csum)
+}
+
+func (IGMP) LayerType() gopacket.LayerType { return layers.LayerTypeIGMP }
diff --git a/internal/bbsimctl/commands/onu.go b/internal/bbsimctl/commands/onu.go
index 620e052..515ba07 100644
--- a/internal/bbsimctl/commands/onu.go
+++ b/internal/bbsimctl/commands/onu.go
@@ -35,6 +35,11 @@
 )
 
 type OnuSnString string
+type IgmpSubAction string
+
+const IgmpJoinKey string = "join"
+const IgmpLeaveKey string = "leave"
+
 type ONUList struct{}
 
 type ONUGet struct {
@@ -67,6 +72,13 @@
 	} `positional-args:"yes" required:"yes"`
 }
 
+type ONUIgmp struct {
+	Args struct {
+		OnuSn     OnuSnString
+		SubAction IgmpSubAction
+	} `positional-args:"yes" required:"yes"`
+}
+
 type ONUOptions struct {
 	List         ONUList         `command:"list"`
 	Get          ONUGet          `command:"get"`
@@ -74,6 +86,7 @@
 	PowerOn      ONUPowerOn      `command:"poweron"`
 	RestartEapol ONUEapolRestart `command:"auth_restart"`
 	RestartDchp  ONUDhcpRestart  `command:"dhcp_restart"`
+	Igmp         ONUIgmp         `command:"igmp"`
 }
 
 func RegisterONUCommands(parser *flags.Parser) {
@@ -228,6 +241,53 @@
 	return nil
 }
 
+func (options *ONUIgmp) Execute(args []string) error {
+	client, conn := connect()
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	req := pb.ONURequest{
+		SerialNumber: string(options.Args.OnuSn),
+	}
+
+	var subActionVal pb.SubActionTypes
+	if string(options.Args.SubAction) == IgmpJoinKey {
+		subActionVal = pb.SubActionTypes_JOIN
+	} else if string(options.Args.SubAction) == IgmpLeaveKey {
+		subActionVal = pb.SubActionTypes_LEAVE
+	}
+
+	igmpReq := pb.IgmpRequest{
+		OnuReq:       &req,
+		SubActionVal: subActionVal,
+	}
+	res, err := client.GetONU(ctx, igmpReq.OnuReq)
+	if err != nil {
+		log.WithFields(log.Fields{
+			"SerialNumber": options.Args.OnuSn,
+		}).Errorf("Cannot not get details on ONU error: %v", err)
+	}
+	log.WithFields(log.Fields{
+		"SerialNumber": igmpReq.OnuReq.SerialNumber,
+	}).Debugf("ONU has indentified : %s", res)
+
+	igmpRes, igmpErr := client.ChangeIgmpState(ctx, &igmpReq)
+	if igmpErr != nil {
+		log.WithFields(log.Fields{
+			"SubAction": options.Args.SubAction,
+		}).Errorf("Could not process Action: error: %v", igmpErr)
+	} else {
+		log.WithFields(log.Fields{
+			"SubAction": options.Args.SubAction,
+		}).Debugf("igmp state has been changed with response: %s",
+			igmpRes.Message)
+	}
+
+	return nil
+}
+
 func (onuSn *OnuSnString) Complete(match string) []flags.Completion {
 	client, conn := connect()
 	defer conn.Close()