[VOL-4111] Moving Services under the UNI struct
Controlling the UNI state via OMCI Set Messages
Upgraded APIs to reflect the new format

Change-Id: I3a6c166205fad4a381e562ab3b873d03b633303e
diff --git a/internal/bbsim/devices/onu.go b/internal/bbsim/devices/onu.go
index 3ee5a7f..db0c747 100644
--- a/internal/bbsim/devices/onu.go
+++ b/internal/bbsim/devices/onu.go
@@ -20,6 +20,9 @@
 	"context"
 	"encoding/hex"
 	"fmt"
+	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
+	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
+	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
 	"sync"
 
 	pb "github.com/opencord/bbsim/api/bbsim"
@@ -29,9 +32,6 @@
 	"strconv"
 	"time"
 
-	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
-	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
-	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
 	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	me "github.com/opencord/omci-lib-go/generated"
 
@@ -103,16 +103,9 @@
 	DiscoveryRetryDelay time.Duration // this is the time between subsequent Discovery Indication
 	DiscoveryDelay      time.Duration // this is the time to send the first Discovery Indication
 
-	Services []ServiceIf
-
 	Backoff *backoff.Backoff
 	// ONU State
-	// PortNo comes with flows and it's used when sending packetIndications,
-	// There is one PortNo per UNI Port, for now we're only storing the first one
-	// FIXME add support for multiple UNIs (each UNI has a different PortNo)
-	// deprecated
-	PortNo   uint32
-	UniPorts []*UniPort
+	UniPorts []UniPortIf
 	Flows    []FlowKey
 	FlowIds  []uint64 // keep track of the flows we currently have in the ONU
 
@@ -149,13 +142,12 @@
 	return common.OnuSnToString(o.SerialNumber)
 }
 
-func CreateONU(olt *OltDevice, pon *PonPort, id uint32, delay time.Duration, isMock bool) *Onu {
+func CreateONU(olt *OltDevice, pon *PonPort, id uint32, delay time.Duration, nextCtag map[string]int, nextStag map[string]int, isMock bool) *Onu {
 
 	o := Onu{
 		ID:                            id,
 		PonPortID:                     pon.ID,
 		PonPort:                       pon,
-		PortNo:                        0,
 		tid:                           0x1,
 		hpTid:                         0x8000,
 		seqNumber:                     0,
@@ -262,11 +254,6 @@
 					},
 				}
 				o.Channel <- msg
-
-				// Once the ONU is enabled start listening for packets
-				for _, s := range o.Services {
-					s.Initialize(o.PonPort.Olt.OpenoltStream)
-				}
 			},
 			fmt.Sprintf("enter_%s", OnuStateDisabled): func(event *fsm.Event) {
 
@@ -291,16 +278,17 @@
 				}
 				o.Channel <- msg
 
+				// disable the UNI ports
+				for _, uni := range o.UniPorts {
+					_ = uni.Disable()
+				}
+
 				// verify all the flows removes are handled and
 				// terminate the ONU's ProcessOnuMessages Go routine
+				// NOTE may need to wait for the UNIs to be down too before shutting down the channel
 				if len(o.FlowIds) == 0 {
 					close(o.Channel)
 				}
-
-				for _, s := range o.Services {
-					s.Disable()
-				}
-
 			},
 			fmt.Sprintf("enter_%s", OnuStatePonDisabled): func(event *fsm.Event) {
 				o.cleanupOnuState()
@@ -322,7 +310,7 @@
 	)
 
 	for i := 0; i < uniPorts; i++ {
-		uni, err := NewUniPort(uint32(i), &o)
+		uni, err := NewUniPort(uint32(i), &o, nextCtag, nextStag)
 		if err != nil {
 			onuLogger.WithFields(log.Fields{
 				"OnuId":  o.ID,
@@ -358,7 +346,6 @@
 // cleanupOnuState this method is to clean the local state when the ONU is disabled
 func (o *Onu) cleanupOnuState() {
 	// clean the ONU state
-	o.PortNo = 0
 	o.Flows = []FlowKey{}
 	o.PonPort.removeOnuId(o.ID)
 	o.PonPort.removeAllocId(o.SerialNumber)
@@ -478,50 +465,55 @@
 					"pktType": msg.Type,
 				}).Trace("Received OnuPacketOut Message")
 
-				if msg.Type == packetHandlers.EAPOL || msg.Type == packetHandlers.DHCP {
+				uni, err := o.findUniByPortNo(msg.PortNo)
 
-					service, err := o.findServiceByMacAddress(msg.MacAddress)
-					if err != nil {
-						onuLogger.WithFields(log.Fields{
-							"IntfId":     msg.IntfId,
-							"OnuId":      msg.OnuId,
-							"pktType":    msg.Type,
-							"MacAddress": msg.MacAddress,
-							"Pkt":        hex.EncodeToString(msg.Packet.Data()),
-							"OnuSn":      o.Sn(),
-						}).Error("Cannot find Service associated with packet")
-						return
-					}
-					service.PacketCh <- msg
-				} else if msg.Type == packetHandlers.IGMP {
-					// if it's an IGMP packet we assume we have a single IGMP service
-					for _, s := range o.Services {
-						service := s.(*Service)
-
-						if service.NeedsIgmp {
-							service.PacketCh <- msg
-						}
-					}
+				if err != nil {
+					onuLogger.WithFields(log.Fields{
+						"IntfId":     msg.IntfId,
+						"OnuId":      msg.OnuId,
+						"pktType":    msg.Type,
+						"portNo":     msg.PortNo,
+						"MacAddress": msg.MacAddress,
+						"Pkt":        hex.EncodeToString(msg.Packet.Data()),
+						"OnuSn":      o.Sn(),
+					}).Error("Cannot find Uni associated with packet")
+					return
 				}
-
+				uni.PacketCh <- msg
+			// BBR specific messages
 			case bbsim.OnuPacketIn:
 				// NOTE we only receive BBR packets here.
 				// Eapol.HandleNextPacket can handle both BBSim and BBr cases so the call is the same
 				// in the DHCP case VOLTHA only act as a proxy, the behaviour is completely different thus we have a dhcp.HandleNextBbrPacket
 				msg, _ := message.Data.(bbsim.OnuPacketMessage)
 
-				log.WithFields(log.Fields{
-					"IntfId":  msg.IntfId,
-					"OnuId":   msg.OnuId,
-					"pktType": msg.Type,
+				onuLogger.WithFields(log.Fields{
+					"IntfId":    msg.IntfId,
+					"OnuId":     msg.OnuId,
+					"PortNo":    msg.PortNo,
+					"GemPortId": msg.GemPortId,
+					"pktType":   msg.Type,
 				}).Trace("Received OnuPacketIn Message")
 
+				uni, err := o.findUniByPortNo(msg.PortNo)
+				if err != nil {
+					onuLogger.WithFields(log.Fields{
+						"IntfId":    msg.IntfId,
+						"OnuId":     msg.OnuId,
+						"PortNo":    msg.PortNo,
+						"GemPortId": msg.GemPortId,
+						"pktType":   msg.Type,
+					}).Error(err.Error())
+				}
+
+				// BBR has one service and one UNI
+				serviceId := uint32(0)
+				oltId := 0
 				if msg.Type == packetHandlers.EAPOL {
-					eapol.HandleNextPacket(msg.OnuId, msg.IntfId, msg.GemPortId, o.Sn(), o.PortNo, o.InternalState, msg.Packet, stream, client)
+					eapol.HandleNextPacket(msg.OnuId, msg.IntfId, msg.GemPortId, o.Sn(), msg.PortNo, uni.ID, serviceId, oltId, o.InternalState, msg.Packet, stream, client)
 				} else if msg.Type == packetHandlers.DHCP {
 					_ = dhcp.HandleNextBbrPacket(o.ID, o.PonPortID, o.Sn(), o.DoneChannel, msg.Packet, client)
 				}
-				// BBR specific messages
 			case bbsim.OmciIndication:
 				// these are OMCI messages received by BBR (VOLTHA emulator)
 				msg, _ := message.Data.(bbsim.OmciIndicationMessage)
@@ -797,46 +789,30 @@
 		switch msgObj.EntityClass {
 		case me.PhysicalPathTerminationPointEthernetUniClassID:
 			// if we're Setting a PPTP state
-			// we need to send the appropriate alarm
-
-			if msgObj.EntityInstance == 257 {
-				// for now we're only caring about the first UNI
-				// NOTE that the EntityID for the UNI port is for now hardcoded in
-				// omci/mibpackets.go where the PhysicalPathTerminationPointEthernetUni
-				// are reported during the MIB Upload sequence
+			// we need to send the appropriate alarm (handled in the UNI struct)
+			uni, err := o.FindUniByEntityId(msgObj.EntityInstance)
+			if err != nil {
+				onuLogger.Error(err)
+				success = false
+			} else {
+				// 1 locks the UNI, 0 unlocks it
 				adminState := msgObj.Attributes["AdministrativeState"].(uint8)
-				raiseOMCIAlarm := false
+				var err error
 				if adminState == 1 {
-					raiseOMCIAlarm = true
-					// set the OperState to disabled
-					if err := o.OperState.Event(OnuTxDisable); err != nil {
-						onuLogger.WithFields(log.Fields{
-							"OnuId":  o.ID,
-							"IntfId": o.PonPortID,
-							"OnuSn":  o.Sn(),
-						}).Errorf("Cannot change ONU OperState to down: %s", err.Error())
-					}
+					err = uni.Disable()
 				} else {
-					// set the OperState to enabled
-					if err := o.OperState.Event(OnuTxEnable); err != nil {
-						onuLogger.WithFields(log.Fields{
-							"OnuId":  o.ID,
-							"IntfId": o.PonPortID,
-							"OnuSn":  o.Sn(),
-						}).Errorf("Cannot change ONU OperState to up: %s", err.Error())
-					}
+					err = uni.Enable()
 				}
-				msg := bbsim.Message{
-					Type: bbsim.UniStatusAlarm,
-					Data: bbsim.UniStatusAlarmMessage{
-						OnuSN:          o.SerialNumber,
-						OnuID:          o.ID,
-						AdminState:     adminState,
-						EntityID:       msgObj.EntityInstance,
-						RaiseOMCIAlarm: raiseOMCIAlarm,
-					},
+				if err != nil {
+					onuLogger.WithFields(log.Fields{
+						"IntfId":       o.PonPortID,
+						"OnuId":        o.ID,
+						"UniMeId":      uni.MeId,
+						"UniId":        uni.ID,
+						"SerialNumber": o.Sn(),
+						"Err":          err.Error(),
+					}).Warn("cannot-change-uni-status")
 				}
-				o.Channel <- msg
 			}
 		case me.TContClassID:
 			allocId := msgObj.Attributes["AllocId"].(uint16)
@@ -1049,7 +1025,6 @@
 			return fmt.Errorf("error-while-processing-create-download-section-response: %s", errResp.Error())
 		}
 		o.ImageSoftwareReceivedSections++
-
 	case omci.EndSoftwareDownloadRequestType:
 
 		// In the startSoftwareDownload we get the image size and the window size.
@@ -1099,7 +1074,6 @@
 				}
 			}
 		}
-
 	case omci.ActivateSoftwareRequestType:
 		if responsePkt, errResp = omcilib.CreateActivateSoftwareResponse(msg.OmciPkt, msg.OmciMsg); errResp == nil {
 			o.MibDataSync++
@@ -1235,23 +1209,27 @@
 	return nil
 }
 
-func (o *Onu) storePortNumber(portNo uint32) {
-	// NOTE this needed only as long as we don't support multiple UNIs
-	// we need to add support for multiple UNIs
-	// the action plan is:
-	// - refactor the omcisim-sim library to use https://github.com/cboling/omci instead of canned messages
-	// - change the library so that it reports a single UNI and remove this workaroung
-	// - add support for multiple UNIs in BBSim
-	if o.PortNo == 0 || portNo < o.PortNo {
-		onuLogger.WithFields(log.Fields{
-			"IntfId":       o.PonPortID,
-			"OnuId":        o.ID,
-			"SerialNumber": o.Sn(),
-			"OnuPortNo":    o.PortNo,
-			"FlowPortNo":   portNo,
-		}).Debug("Storing ONU portNo")
-		o.PortNo = portNo
+// FindUniById retrieves a UNI by ID
+func (o *Onu) FindUniById(uniID uint32) (*UniPort, error) {
+	for _, u := range o.UniPorts {
+		uni := u.(*UniPort)
+		if uni.ID == uniID {
+			return uni, nil
+		}
 	}
+	return nil, fmt.Errorf("cannot-find-uni-with-id-%d-on-onu-%s", uniID, o.Sn())
+}
+
+// FindUniByEntityId retrieves a uni by MeID (the OMCI entity ID)
+func (o *Onu) FindUniByEntityId(meId uint16) (*UniPort, error) {
+	entityId := omcilib.EntityID{}.FromUint16(meId)
+	for _, u := range o.UniPorts {
+		uni := u.(*UniPort)
+		if uni.MeId.Equals(entityId) {
+			return uni, nil
+		}
+	}
+	return nil, fmt.Errorf("cannot-find-uni-with-meid-%s-on-onu-%s", entityId.ToString(), o.Sn())
 }
 
 func (o *Onu) SetID(id uint32) {
@@ -1288,16 +1266,6 @@
 		"PbitToGemport":     msg.Flow.PbitToGemport,
 	}).Debug("OLT receives FlowAdd for ONU")
 
-	if msg.Flow.UniId != 0 {
-		// as of now BBSim only support a single UNI, so ignore everything that is not targeted to it
-		onuLogger.WithFields(log.Fields{
-			"IntfId":       o.PonPortID,
-			"OnuId":        o.ID,
-			"SerialNumber": o.Sn(),
-		}).Debug("Ignoring flow as it's not for the first UNI")
-		return
-	}
-
 	o.FlowIds = append(o.FlowIds, msg.Flow.FlowId)
 
 	var gemPortId uint32
@@ -1310,22 +1278,29 @@
 		// if replicateFlows is false, then the flow is carrying the correct GemPortId
 		gemPortId = uint32(msg.Flow.GemportId)
 	}
-	o.addGemPortToService(gemPortId, msg.Flow.Classifier.EthType, msg.Flow.Classifier.OVid, msg.Flow.Classifier.IVid)
+
+	uni, err := o.FindUniById(uint32(msg.Flow.UniId))
+	if err != nil {
+		onuLogger.WithFields(log.Fields{
+			"IntfId":       o.PonPortID,
+			"OnuId":        o.ID,
+			"UniId":        msg.Flow.UniId,
+			"PortNo":       msg.Flow.PortNo,
+			"SerialNumber": o.Sn(),
+			"FlowId":       msg.Flow.FlowId,
+			"FlowType":     msg.Flow.FlowType,
+		}).Error("cannot-find-uni-port-for-flow")
+	}
+
+	uni.addGemPortToService(gemPortId, msg.Flow.Classifier.EthType, msg.Flow.Classifier.OVid, msg.Flow.Classifier.IVid)
+	uni.StorePortNo(msg.Flow.PortNo)
 
 	if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeEAPOL) && msg.Flow.Classifier.OVid == 4091 {
-		// NOTE storing the PortNO, it's needed when sending PacketIndications
-		o.storePortNumber(uint32(msg.Flow.PortNo))
-
-		for _, s := range o.Services {
-			s.HandleAuth()
-		}
+		uni.HandleAuth()
 	} else if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeIPv4) &&
 		msg.Flow.Classifier.SrcPort == uint32(68) &&
 		msg.Flow.Classifier.DstPort == uint32(67) {
-
-		for _, s := range o.Services {
-			s.HandleDhcp(uint8(msg.Flow.Classifier.OPbits), int(msg.Flow.Classifier.OVid))
-		}
+		uni.HandleDhcp(uint8(msg.Flow.Classifier.OPbits), int(msg.Flow.Classifier.OVid))
 	}
 }
 
@@ -1426,6 +1401,7 @@
 }
 
 // TODO move this method in responders/omcisim
+// StartOmci is called in BBR to start the OMCI state machine
 func (o *Onu) StartOmci(client openolt.OpenoltClient) {
 	mibReset, _ := omcilib.CreateMibResetRequest(o.getNextTid(false))
 	sendOmciMsg(mibReset, o.PonPortID, o.ID, o.SerialNumber, "mibReset", client)
@@ -1470,21 +1446,42 @@
 		sendOmciMsg(mibUploadNext, o.PonPortID, o.ID, o.SerialNumber, "mibUploadNext", client)
 	case omci.MibUploadNextResponseType:
 		o.seqNumber++
+		// once the mibUpload is complete send a SetRequest for the PPTP to enable the UNI
+		// NOTE that in BBR we only enable the first UNI
+		if o.seqNumber == o.MibDb.NumberOfCommands {
+			meId := omcilib.GenerateUniPortEntityId(1)
 
-		if o.seqNumber > 290 {
-			// NOTE we are done with the MIB Upload (290 is the number of messages the omci-sim library will respond to)
-			// start sending the flows, we don't care about the OMCI setup in BBR, just that a lot of messages can go through
-			if err := o.InternalState.Event(BbrOnuTxSendEapolFlow); err != nil {
+			meParams := me.ParamData{
+				EntityID:   meId.ToUint16(),
+				Attributes: me.AttributeValueMap{"AdministrativeState": 0},
+			}
+			managedEntity, omciError := me.NewPhysicalPathTerminationPointEthernetUni(meParams)
+			if omciError.GetError() != nil {
 				onuLogger.WithFields(log.Fields{
 					"OnuId":  o.ID,
 					"IntfId": o.PonPortID,
 					"OnuSn":  o.Sn(),
-				}).Errorf("Error while transitioning ONU State %v", err)
+				}).Fatal(omciError.GetError())
 			}
+
+			setPPtp, _ := omcilib.CreateSetRequest(managedEntity, 1)
+			sendOmciMsg(setPPtp, o.PonPortID, o.ID, o.SerialNumber, "setRquest", client)
 		} else {
 			mibUploadNext, _ := omcilib.CreateMibUploadNextRequest(o.getNextTid(false), o.seqNumber)
 			sendOmciMsg(mibUploadNext, o.PonPortID, o.ID, o.SerialNumber, "mibUploadNext", client)
 		}
+	case omci.SetResponseType:
+		// once we set the PPTP to active we can start sending flows
+
+		if err := o.InternalState.Event(BbrOnuTxSendEapolFlow); err != nil {
+			onuLogger.WithFields(log.Fields{
+				"OnuId":  o.ID,
+				"IntfId": o.PonPortID,
+				"OnuSn":  o.Sn(),
+			}).Errorf("Error while transitioning ONU State %v", err)
+		}
+	case omci.AlarmNotificationType:
+		log.Info("bbr-received-alarm")
 	}
 }
 
@@ -1536,14 +1533,14 @@
 
 func (o *Onu) sendDhcpFlow(client openolt.OpenoltClient) {
 
-	// BBR only works with a single service (ATT HSIA)
-	hsia := o.Services[0].(*Service)
-
+	// BBR only works with a single UNI and a single service (ATT HSIA)
+	hsia := o.UniPorts[0].(*UniPort).Services[0].(*Service)
 	classifierProto := openolt.Classifier{
 		EthType: uint32(layers.EthernetTypeIPv4),
 		SrcPort: uint32(68),
 		DstPort: uint32(67),
 		OVid:    uint32(hsia.CTag),
+		OPbits:  255,
 	}
 
 	actionProto := openolt.Action{}
@@ -1551,7 +1548,7 @@
 	downstreamFlow := openolt.Flow{
 		AccessIntfId:  int32(o.PonPortID),
 		OnuId:         int32(o.ID),
-		UniId:         int32(0), // FIXME do not hardcode this
+		UniId:         int32(0), // BBR only supports a single UNI
 		FlowId:        uint64(o.ID),
 		FlowType:      "downstream",
 		NetworkIntfId: int32(0),
@@ -1627,39 +1624,31 @@
 	}
 }
 
-func (onu *Onu) addGemPortToService(gemport uint32, ethType uint32, oVlan uint32, iVlan uint32) {
-	for _, s := range onu.Services {
-		if service, ok := s.(*Service); ok {
-			// EAPOL is a strange case, as packets are untagged
-			// but we assume we will have a single service requiring EAPOL
-			if ethType == uint32(layers.EthernetTypeEAPOL) && service.NeedsEapol {
-				service.GemPort = gemport
-			}
-
-			// For DHCP services we single tag the outgoing packets,
-			// thus the flow only contains the CTag and we can use that to match the service
-			if ethType == uint32(layers.EthernetTypeIPv4) && service.NeedsDhcp && service.CTag == int(oVlan) {
-				service.GemPort = gemport
-			}
-
-			// for dataplane services match both C and S tags
-			if service.CTag == int(iVlan) && service.STag == int(oVlan) {
-				service.GemPort = gemport
-			}
-		}
-	}
-}
-
+// deprecated, delegate this to the uniPort
 func (onu *Onu) findServiceByMacAddress(macAddress net.HardwareAddr) (*Service, error) {
-	for _, s := range onu.Services {
-		service := s.(*Service)
-		if service.HwAddress.String() == macAddress.String() {
-			return service, nil
+	// FIXME is there a better way to avoid this loop?
+	for _, u := range onu.UniPorts {
+		uni := u.(*UniPort)
+		for _, s := range uni.Services {
+			service := s.(*Service)
+			if service.HwAddress.String() == macAddress.String() {
+				return service, nil
+			}
 		}
 	}
 	return nil, fmt.Errorf("cannot-find-service-with-mac-address-%s", macAddress.String())
 }
 
+func (onu *Onu) findUniByPortNo(portNo uint32) (*UniPort, error) {
+	for _, u := range onu.UniPorts {
+		uni := u.(*UniPort)
+		if uni.PortNo == portNo {
+			return uni, nil
+		}
+	}
+	return nil, fmt.Errorf("cannot-find-uni-with-port-no-%d", portNo)
+}
+
 func (o *Onu) SendOMCIAlarmNotificationMsg(raiseOMCIAlarm bool, alarmType string) {
 	switch alarmType {
 	case "ONU_ALARM_LOS":