[SEBA-918][VOL-2984] Allowing DHCP without completing eapol

For DT and TT workflow is required that DHCP is triggered without doing
anything for EAPOL. If you start BBSim with only the "-dhcp" option that
is now possible.

Change-Id: Iacfb75e70c47a2f7cfa64af58d6d848881f54974
diff --git a/internal/bbsim/devices/onu.go b/internal/bbsim/devices/onu.go
index ecb91f8..6b879cc 100644
--- a/internal/bbsim/devices/onu.go
+++ b/internal/bbsim/devices/onu.go
@@ -65,14 +65,17 @@
 	// 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)
-	PortNo           uint32
-	DhcpFlowReceived bool
-	Flows            []FlowKey
+	PortNo            uint32
+	GemPortAdded      bool
+	EapolFlowReceived bool
+	DhcpFlowReceived  bool
+	Flows             []FlowKey
 
 	OperState    *fsm.FSM
 	SerialNumber *openolt.SerialNumber
 
-	Channel chan Message // this Channel is to track state changes OMCI messages, EAPOL and DHCP packets
+	Channel         chan Message // this Channel is to track state changes OMCI messages, EAPOL and DHCP packets
+	GemPortChannels []chan bool  // this channels are used to notify everyone that is interested that a GemPort has been added
 
 	// OMCI params
 	tid        uint16
@@ -88,6 +91,12 @@
 	return common.OnuSnToString(o.SerialNumber)
 }
 
+func (o *Onu) GetGemPortChan() chan bool {
+	listener := make(chan bool, 1)
+	o.GemPortChannels = append(o.GemPortChannels, listener)
+	return listener
+}
+
 func CreateONU(olt *OltDevice, pon PonPort, id uint32, sTag int, cTag int, auth bool, dhcp bool, delay time.Duration, isMock bool) *Onu {
 
 	o := Onu{
@@ -105,6 +114,8 @@
 		seqNumber:           0,
 		DoneChannel:         make(chan bool, 1),
 		DhcpFlowReceived:    false,
+		EapolFlowReceived:   false,
+		GemPortAdded:        false,
 		DiscoveryRetryDelay: 60 * time.Second, // this is used to send OnuDiscoveryIndications until an activate call is received
 		Flows:               []FlowKey{},
 		DiscoveryDelay:      delay,
@@ -130,18 +141,18 @@
 			{Name: "receive_eapol_flow", Src: []string{"enabled", "gem_port_added"}, Dst: "eapol_flow_received"},
 			{Name: "add_gem_port", Src: []string{"enabled", "eapol_flow_received"}, Dst: "gem_port_added"},
 			// NOTE should disabled state be different for oper_disabled (emulating an error) and admin_disabled (received a disabled call via VOLTHA)?
-			{Name: "disable", Src: []string{"enabled", "eapol_flow_received", "gem_port_added", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed", "pon_disabled"}, Dst: "disabled"},
+			{Name: "disable", Src: []string{"enabled", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed", "pon_disabled"}, Dst: "disabled"},
 			// ONU state when PON port is disabled but ONU is power ON(more states should be added in src?)
-			{Name: "pon_disabled", Src: []string{"enabled", "gem_port_added", "eapol_flow_received", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed"}, Dst: "pon_disabled"},
+			{Name: "pon_disabled", Src: []string{"enabled", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed"}, Dst: "pon_disabled"},
 			// EAPOL
-			{Name: "start_auth", Src: []string{"eapol_flow_received", "gem_port_added", "eap_start_sent", "eap_response_identity_sent", "eap_response_challenge_sent", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed", "igmp_join_started", "igmp_left", "igmp_join_error"}, Dst: "auth_started"},
+			{Name: "start_auth", Src: []string{"enabled", "eap_start_sent", "eap_response_identity_sent", "eap_response_challenge_sent", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed", "igmp_join_started", "igmp_left", "igmp_join_error"}, Dst: "auth_started"},
 			{Name: "eap_start_sent", Src: []string{"auth_started"}, Dst: "eap_start_sent"},
 			{Name: "eap_response_identity_sent", Src: []string{"eap_start_sent"}, Dst: "eap_response_identity_sent"},
 			{Name: "eap_response_challenge_sent", Src: []string{"eap_response_identity_sent"}, Dst: "eap_response_challenge_sent"},
 			{Name: "eap_response_success_received", Src: []string{"eap_response_challenge_sent"}, Dst: "eap_response_success_received"},
 			{Name: "auth_failed", Src: []string{"auth_started", "eap_start_sent", "eap_response_identity_sent", "eap_response_challenge_sent"}, Dst: "auth_failed"},
 			// DHCP
-			{Name: "start_dhcp", Src: []string{"eap_response_success_received", "dhcp_discovery_sent", "dhcp_request_sent", "dhcp_ack_received", "dhcp_failed", "igmp_join_started", "igmp_left", "igmp_join_error"}, Dst: "dhcp_started"},
+			{Name: "start_dhcp", Src: []string{"enabled", "eap_response_success_received", "dhcp_discovery_sent", "dhcp_request_sent", "dhcp_ack_received", "dhcp_failed", "igmp_join_started", "igmp_left", "igmp_join_error"}, Dst: "dhcp_started"},
 			{Name: "dhcp_discovery_sent", Src: []string{"dhcp_started"}, Dst: "dhcp_discovery_sent"},
 			{Name: "dhcp_request_sent", Src: []string{"dhcp_discovery_sent"}, Dst: "dhcp_request_sent"},
 			{Name: "dhcp_ack_received", Src: []string{"dhcp_request_sent"}, Dst: "dhcp_ack_received"},
@@ -151,10 +162,10 @@
 			{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", "dhcp_ack_received", "igmp_left", "igmp_join_error", "igmp_join_started"}, Dst: "igmp_join_started"},
-			{Name: "igmp_join_startv3", Src: []string{"eap_response_success_received", "gem_port_added", "eapol_flow_received", "dhcp_ack_received", "igmp_left", "igmp_join_error", "igmp_join_started"}, Dst: "igmp_join_started"},
+			{Name: "igmp_join_start", Src: []string{"eap_response_success_received", "dhcp_ack_received", "igmp_left", "igmp_join_error", "igmp_join_started"}, Dst: "igmp_join_started"},
+			{Name: "igmp_join_startv3", Src: []string{"eap_response_success_received", "dhcp_ack_received", "igmp_left", "igmp_join_error", "igmp_join_started"}, Dst: "igmp_join_started"},
 			{Name: "igmp_join_error", Src: []string{"igmp_join_started"}, Dst: "igmp_join_error"},
-			{Name: "igmp_leave", Src: []string{"igmp_join_started", "gem_port_added", "eapol_flow_received", "eap_response_success_received", "dhcp_ack_received"}, Dst: "igmp_left"},
+			{Name: "igmp_leave", Src: []string{"igmp_join_started", "eap_response_success_received", "dhcp_ack_received"}, Dst: "igmp_left"},
 		},
 		fsm.Callbacks{
 			"enter_state": func(e *fsm.Event) {
@@ -227,6 +238,16 @@
 				// terminate the ONU's ProcessOnuMessages Go routine
 				close(o.Channel)
 			},
+			"before_start_auth": func(e *fsm.Event) {
+				if o.EapolFlowReceived == false {
+					e.Cancel(errors.New("cannot-go-to-auth-started-as-eapol-flow-is-missing"))
+					return
+				}
+				if o.GemPortAdded == false {
+					e.Cancel(errors.New("cannot-go-to-auth-started-as-gemport-is-missing"))
+					return
+				}
+			},
 			"enter_auth_started": func(e *fsm.Event) {
 				o.logStateChange(e.Src, e.Dst)
 				msg := Message{
@@ -249,8 +270,21 @@
 				}).Errorf("ONU failed to authenticate!")
 			},
 			"before_start_dhcp": func(e *fsm.Event) {
+
+				// we allow transition from eanbled to dhcp_started only if auth was set to false
+				if o.InternalState.Current() == "enabled" && o.Auth {
+					e.Cancel(errors.New("cannot-go-to-dhcp-started-as-authentication-is-required"))
+					return
+				}
+
 				if o.DhcpFlowReceived == false {
 					e.Cancel(errors.New("cannot-go-to-dhcp-started-as-dhcp-flow-is-missing"))
+					return
+				}
+
+				if o.GemPortAdded == false {
+					e.Cancel(errors.New("cannot-go-to-dhcp-started-as-gemport-is-missing"))
+					return
 				}
 			},
 			"enter_dhcp_started": func(e *fsm.Event) {
@@ -471,25 +505,16 @@
 			"OnuSn":  o.Sn(),
 		}).Infof("GemPort Added")
 
-		// NOTE if we receive the GemPort but we don't have EAPOL flows
-		// go an intermediate state, otherwise start auth
-		if o.InternalState.Is("enabled") {
-			if err := o.InternalState.Event("add_gem_port"); err != nil {
-				log.Errorf("Can't go to gem_port_added: %v", err)
-			}
-		} else if o.InternalState.Is("eapol_flow_received") {
-			if o.Auth == true {
-				if err := o.InternalState.Event("start_auth"); err != nil {
-					log.Warnf("Can't go to auth_started: %v", err)
-				}
-			} else {
-				onuLogger.WithFields(log.Fields{
-					"IntfId":       o.PonPortID,
-					"OnuId":        o.ID,
-					"SerialNumber": o.Sn(),
-				}).Warn("Not starting authentication as Auth bit is not set in CLI parameters")
-			}
+		o.GemPortAdded = true
+
+		// broadcast the change to all listeners
+		// and close the channels as once the GemPort is set
+		// it won't change anymore
+		for _, ch := range o.GemPortChannels {
+			ch <- true
+			close(ch)
 		}
+		o.GemPortChannels = []chan bool{}
 	}
 }
 
@@ -721,34 +746,41 @@
 	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))
-
-		// NOTE if we receive the EAPOL flows but we don't have GemPorts
-		// go an intermediate state, otherwise start auth
-		if o.InternalState.Is("enabled") {
-			if err := o.InternalState.Event("receive_eapol_flow"); err != nil {
-				log.Warnf("Can't go to eapol_flow_received: %v", err)
-			}
-		} else if o.InternalState.Is("gem_port_added") {
-
-			if o.Auth == true {
-				if err := o.InternalState.Event("start_auth"); err != nil {
-					log.Warnf("Can't go to auth_started: %v", err)
-				}
+		o.EapolFlowReceived = true
+		// if authentication is not enabled, do nothing
+		if o.Auth {
+			// NOTE if we receive the EAPOL flows but we don't have GemPorts
+			// wait for it before starting auth
+			if !o.GemPortAdded {
+				// wait for Gem and then start auth
+				go func() {
+					for v := range o.GetGemPortChan() {
+						if v == true {
+							if err := o.InternalState.Event("start_auth"); err != nil {
+								onuLogger.Warnf("Can't go to auth_started: %v", err)
+							}
+						}
+					}
+					onuLogger.Trace("GemPortChannel closed")
+				}()
 			} else {
-				onuLogger.WithFields(log.Fields{
-					"IntfId":       o.PonPortID,
-					"OnuId":        o.ID,
-					"SerialNumber": o.Sn(),
-				}).Warn("Not starting authentication as Auth bit is not set in CLI parameters")
+				// start the EAPOL state machine
+				if err := o.InternalState.Event("start_auth"); err != nil {
+					onuLogger.Warnf("Can't go to auth_started: %v", err)
+				}
 			}
-
+		} else {
+			onuLogger.WithFields(log.Fields{
+				"IntfId":       o.PonPortID,
+				"OnuId":        o.ID,
+				"SerialNumber": o.Sn(),
+			}).Warn("Not starting authentication as Auth bit is not set in CLI parameters")
 		}
 	} else if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeIPv4) &&
 		msg.Flow.Classifier.SrcPort == uint32(68) &&
 		msg.Flow.Classifier.DstPort == uint32(67) &&
 		(msg.Flow.Classifier.OPbits == 0 || msg.Flow.Classifier.OPbits == 255) {
 
-
 		if o.Dhcp == true {
 			if o.DhcpFlowReceived == false {
 				// keep track that we received the DHCP Flows
@@ -756,9 +788,22 @@
 				// this is needed as a check in case someone trigger DHCP from the CLI
 				o.DhcpFlowReceived = true
 
-				// now start the DHCP state machine
-				if err := o.InternalState.Event("start_dhcp"); err != nil {
-					log.Errorf("Can't go to dhcp_started: %v", err)
+				if !o.GemPortAdded {
+					// wait for Gem and then start DHCP
+					go func() {
+						for v := range o.GetGemPortChan() {
+							if v == true {
+								if err := o.InternalState.Event("start_dhcp"); err != nil {
+									log.Errorf("Can't go to dhcp_started: %v", err)
+								}
+							}
+						}
+					}()
+				} else {
+					// start the DHCP state machine
+					if err := o.InternalState.Event("start_dhcp"); err != nil {
+						log.Errorf("Can't go to dhcp_started: %v", err)
+					}
 				}
 			} else {
 				onuLogger.WithFields(log.Fields{