From 5b039be6baf201e2e54d625bc60fa0b8d64b47fa Mon Sep 17 00:00:00 2001 From: Raguideau Date: Mon, 4 Mar 2024 16:37:36 +0000 Subject: [PATCH 1/9] add scenario manager Signed-off-by: Raguideau --- config/config.go | 18 +- config/config.yml | 2 +- internal/common/tools/tools.go | 2 +- internal/control_test_engine/gnb/gnb.go | 50 ++++ internal/control_test_engine/ue/ue.go | 26 +- internal/scenario/scenario.go | 223 ++++++++++++++++++ internal/scenario/task.go | 29 +++ internal/templates/test-custom-scenario.go | 2 +- internal/templates/test-multi-ues-in-queue.go | 68 ++++++ test/pr_test.go | 68 +++++- 10 files changed, 462 insertions(+), 26 deletions(-) create mode 100644 internal/scenario/scenario.go create mode 100644 internal/scenario/task.go diff --git a/config/config.go b/config/config.go index 1b37aeab..8d471ab8 100644 --- a/config/config.go +++ b/config/config.go @@ -195,7 +195,7 @@ func setLogLevel(cfg Config) { } -func (config *Config) GetUESecurityCapability() *nasType.UESecurityCapability { +func (ue *Ue) GetUESecurityCapability() *nasType.UESecurityCapability { UESecurityCapability := &nasType.UESecurityCapability{ Iei: nasMessage.RegistrationRequestUESecurityCapabilityType, Len: 2, @@ -203,16 +203,16 @@ func (config *Config) GetUESecurityCapability() *nasType.UESecurityCapability { } // Ciphering algorithms - UESecurityCapability.SetEA0_5G(boolToUint8(config.Ue.Ciphering.Nea0)) - UESecurityCapability.SetEA1_128_5G(boolToUint8(config.Ue.Ciphering.Nea1)) - UESecurityCapability.SetEA2_128_5G(boolToUint8(config.Ue.Ciphering.Nea2)) - UESecurityCapability.SetEA3_128_5G(boolToUint8(config.Ue.Ciphering.Nea3)) + UESecurityCapability.SetEA0_5G(boolToUint8(ue.Ciphering.Nea0)) + UESecurityCapability.SetEA1_128_5G(boolToUint8(ue.Ciphering.Nea1)) + UESecurityCapability.SetEA2_128_5G(boolToUint8(ue.Ciphering.Nea2)) + UESecurityCapability.SetEA3_128_5G(boolToUint8(ue.Ciphering.Nea3)) // Integrity algorithms - UESecurityCapability.SetIA0_5G(boolToUint8(config.Ue.Integrity.Nia0)) - UESecurityCapability.SetIA1_128_5G(boolToUint8(config.Ue.Integrity.Nia1)) - UESecurityCapability.SetIA2_128_5G(boolToUint8(config.Ue.Integrity.Nia2)) - UESecurityCapability.SetIA3_128_5G(boolToUint8(config.Ue.Integrity.Nia3)) + UESecurityCapability.SetIA0_5G(boolToUint8(ue.Integrity.Nia0)) + UESecurityCapability.SetIA1_128_5G(boolToUint8(ue.Integrity.Nia1)) + UESecurityCapability.SetIA2_128_5G(boolToUint8(ue.Integrity.Nia2)) + UESecurityCapability.SetIA3_128_5G(boolToUint8(ue.Integrity.Nia3)) return UESecurityCapability } diff --git a/config/config.yml b/config/config.yml index 58bdcc70..af6400d4 100644 --- a/config/config.yml +++ b/config/config.yml @@ -14,7 +14,7 @@ gnodeb: sst: "01" sd: "000001" # optional, can be removed if not used ue: - msin: "0000000120" + msin: "0000001000" key: "00112233445566778899AABBCCDDEEFF" opc: "00112233445566778899AABBCCDDEEFF" amf: "8000" diff --git a/internal/common/tools/tools.go b/internal/common/tools/tools.go index a3dae7bc..45f71d73 100644 --- a/internal/common/tools/tools.go +++ b/internal/common/tools/tools.go @@ -127,7 +127,7 @@ func SimulateSingleUE(simConfig UESimulationConfig, wg *sync.WaitGroup) { // Create a new UE coroutine // ue.NewUE returns context of the new UE - ueTx := ue.NewUE(ueCfg, ueId, ueRx, simConfig.Gnbs[gnbIdGen(0)].GetInboundChannel(), wg) + ueTx := ue.NewUE(ueCfg.Ue, ueId, ueRx, simConfig.Gnbs[gnbIdGen(0)].GetInboundChannel(), wg) // We tell the UE to perform a registration ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} diff --git a/internal/control_test_engine/gnb/gnb.go b/internal/control_test_engine/gnb/gnb.go index d4c218b1..ca2203a5 100644 --- a/internal/control_test_engine/gnb/gnb.go +++ b/internal/control_test_engine/gnb/gnb.go @@ -71,6 +71,56 @@ func InitGnb(conf config.Config, wg *sync.WaitGroup) *context.GNBContext { return gnb } +func InitGnb2(gnbConf config.GNodeB, amfConf config.AMF, wg *sync.WaitGroup) *context.GNBContext { + + // instance new gnb. + gnb := &context.GNBContext{} + + // new gnb context. + gnb.NewRanGnbContext( + gnbConf.PlmnList.GnbId, + gnbConf.PlmnList.Mcc, + gnbConf.PlmnList.Mnc, + gnbConf.PlmnList.Tac, + gnbConf.SliceSupportList.Sst, + gnbConf.SliceSupportList.Sd, + gnbConf.ControlIF.Ip, + gnbConf.DataIF.Ip, + gnbConf.ControlIF.Port, + gnbConf.DataIF.Port) + + // start communication with AMF (server SCTP). + + // new AMF context. + amf := gnb.NewGnBAmf(amfConf.Ip, amfConf.Port) + + // start communication with AMF(SCTP). + if err := serviceNgap.InitConn(amf, gnb); err != nil { + log.Fatal("Error in", err) + } else { + log.Info("[GNB] SCTP/NGAP service is running") + // wg.Add(1) + } + + // start communication with UE (server UNIX sockets). + serviceNas.InitServer(gnb) + + trigger.SendNgSetupRequest(gnb, amf) + + go func() { + // control the signals + sigGnb := make(chan os.Signal, 1) + signal.Notify(sigGnb, os.Interrupt) + + // Block until a signal is received. + <-sigGnb + //gnb.Terminate() + wg.Done() + }() + + return gnb +} + func InitGnbForLoadSeconds(conf config.Config, wg *sync.WaitGroup, monitor *monitoring.Monitor) { diff --git a/internal/control_test_engine/ue/ue.go b/internal/control_test_engine/ue/ue.go index 482b825a..8af11771 100644 --- a/internal/control_test_engine/ue/ue.go +++ b/internal/control_test_engine/ue/ue.go @@ -22,27 +22,27 @@ import ( log "github.com/sirupsen/logrus" ) -func NewUE(conf config.Config, id int, ueMgrChannel chan procedures.UeTesterMessage, gnbInboundChannel chan context2.UEMessage, wg *sync.WaitGroup) chan scenario.ScenarioMessage { +func NewUE(conf config.Ue, id int, ueMgrChannel chan procedures.UeTesterMessage, gnbInboundChannel chan context2.UEMessage, wg *sync.WaitGroup) chan scenario.ScenarioMessage { // new UE instance. ue := &context.UEContext{} scenarioChan := make(chan scenario.ScenarioMessage) // new UE context ue.NewRanUeContext( - conf.Ue.Msin, + conf.Msin, conf.GetUESecurityCapability(), - conf.Ue.Key, - conf.Ue.Opc, + conf.Key, + conf.Opc, "c9e8763286b5b9ffbdf56e1297d0887b", - conf.Ue.Amf, - conf.Ue.Sqn, - conf.Ue.Hplmn.Mcc, - conf.Ue.Hplmn.Mnc, - conf.Ue.RoutingIndicator, - conf.Ue.Dnn, - int32(conf.Ue.Snssai.Sst), - conf.Ue.Snssai.Sd, - conf.Ue.TunnelMode, + conf.Amf, + conf.Sqn, + conf.Hplmn.Mcc, + conf.Hplmn.Mnc, + conf.RoutingIndicator, + conf.Dnn, + int32(conf.Snssai.Sst), + conf.Snssai.Sd, + conf.TunnelMode, scenarioChan, gnbInboundChannel, id) diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go new file mode 100644 index 00000000..4134f264 --- /dev/null +++ b/internal/scenario/scenario.go @@ -0,0 +1,223 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * © Copyright 2023 Hewlett Packard Enterprise Development LP + */ +package scenario + +import ( + "fmt" + "my5G-RANTester/config" + "my5G-RANTester/internal/control_test_engine/gnb" + "my5G-RANTester/internal/control_test_engine/gnb/context" + "my5G-RANTester/internal/control_test_engine/gnb/ngap/trigger" + "my5G-RANTester/internal/control_test_engine/procedures" + "my5G-RANTester/internal/control_test_engine/ue" + ueCtx "my5G-RANTester/internal/control_test_engine/ue/context" + "my5G-RANTester/internal/control_test_engine/ue/scenario" + "os" + "os/signal" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +type Runner struct { + gnbs map[string]*context.GNBContext + ues map[int]*ueSim +} + +type ueSim struct { + ongoingTask bool + nextTasks []Task + taskChan chan Task + reportChan chan report +} + +type UEScenario struct { + Config config.Ue + Tasks []Task +} + +type report struct { + success bool + reason string +} + +func (s *Runner) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario, maxRequestRate int) { + + wg := sync.WaitGroup{} + + s.gnbs = make(map[string]*context.GNBContext) + + for gnbConf := range gnbConfs { + s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = gnb.InitGnb2(gnbConfs[gnbConf], amfConf, &wg) // TODO: replace InitGNB2 with InitGNB + wg.Add(1) + } + + // Wait for gNB to be connected before registering UEs + // TODO: We should wait for NGSetupResponse instead + time.Sleep(2 * time.Second) + + s.ues = make(map[int]*ueSim) + + sigStop := make(chan os.Signal, 1) + signal.Notify(sigStop, os.Interrupt) + stopSignal := false + for !stopSignal { + done := true + for ueId := 1; !stopSignal && ueId <= len(ueScenarios); ueId++ { + if s.ues[ueId] == nil { + ue := ueSim{ + ongoingTask: false, + nextTasks: ueScenarios[ueId-1].Tasks, + taskChan: make(chan Task, 1), + reportChan: make(chan report, 1), + } + + go simulateSingleUE(ueId, ueScenarios[ueId-1].Config, ue.taskChan, ue.reportChan, s.gnbs, &wg) + + s.ues[ueId] = &ue + } + + select { + case report := <-s.ues[ueId].reportChan: + + if !report.success { + s.ues[ueId].nextTasks = []Task{} + } + s.ues[ueId].ongoingTask = false + case <-sigStop: + stopSignal = true + default: + } + + if s.ues[ueId].ongoingTask { + done = false + } else { + if len(s.ues[ueId].nextTasks) > 0 { + s.ues[ueId].taskChan <- s.ues[ueId].nextTasks[0] + s.ues[ueId].nextTasks = s.ues[ueId].nextTasks[1:] + s.ues[ueId].ongoingTask = true + done = false + } + } + + } + if done { + stopSignal = true + } + } + for _, ue := range s.ues { + ue.taskChan <- Task{ + TaskType: Terminate, + } + } + + time.Sleep(time.Second * 1) +} + +func simulateSingleUE(ueId int, ueCfg config.Ue, taskChan chan Task, reportChan chan report, gnbs map[string]*context.GNBContext, wg *sync.WaitGroup) { + log.Infof("simulation started for ue %d", ueId) + + wg.Add(1) + + ueRx := make(chan procedures.UeTesterMessage) + var ueTx chan scenario.ScenarioMessage + + loop := true + var attachedGnb string + state := ueCtx.MM5G_NULL + targetState := ueCtx.MM5G_NULL + for loop { + select { + case task := <-taskChan: + if attachedGnb == "" && task.TaskType != AttachToGNB { + msg := fmt.Sprintf("UE %d is not attached to a GNB", ueId) + log.Error(msg) + reportChan <- report{success: false, reason: msg} + } + switch task.TaskType { + case AttachToGNB: + if gnbs[task.Parameters.GnbId] != nil { + // Create a new UE coroutine + // ue.NewUE returns context of the new UE + ueTx = ue.NewUE(ueCfg, ueId, ueRx, gnbs[task.Parameters.GnbId].GetInboundChannel(), wg) + attachedGnb = task.Parameters.GnbId + reportChan <- report{success: true} + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + log.Error(msg) + reportChan <- report{success: false, reason: msg} + } + case Registration: + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} }) + targetState = ueCtx.MM5G_REGISTERED + case Deregistration: + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.Deregistration} }) + targetState = ueCtx.MM5G_DEREGISTERED + case Idle: + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.Idle} }) + targetState = ueCtx.MM5G_IDLE + case NewPDUSession: + if state == ueCtx.MM5G_REGISTERED { // TODO: check if it's possible to send pdu req without being registrated (for negative tests) + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} }) + if !task.Hang { + reportChan <- report{success: true, reason: "Need to implement way to check if pduSession is successful"} + } + } else { + msg := fmt.Sprintf("UE %d is not registered", ueId) + log.Error(msg) + reportChan <- report{success: false, reason: msg} + } + case XNHandover: + if gnbs[task.Parameters.GnbId] != nil { + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + trigger.TriggerXnHandover(gnbs[attachedGnb], gnbs[task.Parameters.GnbId], int64(ueId)) + }) + targetState = ueCtx.MM5G_REGISTERED + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + log.Error(msg) + reportChan <- report{success: false, reason: msg} + } + case NGAPHandover: + if gnbs[task.Parameters.GnbId] != nil { + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + trigger.TriggerNgapHandover(gnbs[attachedGnb], gnbs[task.Parameters.GnbId], int64(ueId)) + }) + targetState = ueCtx.MM5G_REGISTERED + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + log.Error(msg) + reportChan <- report{success: false, reason: msg} + } + case Terminate: + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + ueRx <- procedures.UeTesterMessage{Type: procedures. + Terminate} + ueRx = nil + }) + case Kill: + time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + ueRx <- procedures.UeTesterMessage{Type: procedures. + Kill} + ueRx = nil + state = ueCtx.MM5G_NULL + attachedGnb = "" + }) + } + + case ueMsg := <-ueTx: + msg := fmt.Sprint("[UE] Switched from state ", state, " to state ", ueMsg.StateChange) + log.Info(msg) + if ueMsg.StateChange == targetState { + reportChan <- report{success: true, reason: msg} + } + if ueMsg.StateChange == ueCtx.MM5G_NULL { + loop = false + } + state = ueMsg.StateChange + } + } +} diff --git a/internal/scenario/task.go b/internal/scenario/task.go new file mode 100644 index 00000000..3fb3cb72 --- /dev/null +++ b/internal/scenario/task.go @@ -0,0 +1,29 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * © Copyright 2023 Hewlett Packard Enterprise Development LP + */ +package scenario + +type Task struct { + TaskType TaskType + Delay int + Hang bool // hang simulation in this state until ctrl-c is received + Parameters struct { + GnbId string + } +} + +type TaskType int32 + +const ( + AttachToGNB TaskType = iota + Registration + Deregistration + NewPDUSession + // DestroyPDUSession + Terminate + Kill + Idle + NGAPHandover + XNHandover +) diff --git a/internal/templates/test-custom-scenario.go b/internal/templates/test-custom-scenario.go index 363325db..f4c1be82 100644 --- a/internal/templates/test-custom-scenario.go +++ b/internal/templates/test-custom-scenario.go @@ -33,7 +33,7 @@ func TestWithCustomScenario(scenarioPath string) { wg.Add(1) - _ = ue.NewUE(cfg, 1, ueChan, gnb.GetInboundChannel(), &wg) + _ = ue.NewUE(cfg.Ue, 1, ueChan, gnb.GetInboundChannel(), &wg) ctx, runtime := script.NewCustomScenario(scenarioPath) diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index 7e5d99c9..a7d0d7aa 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -8,6 +8,7 @@ import ( "my5G-RANTester/config" "my5G-RANTester/internal/common/tools" "my5G-RANTester/internal/control_test_engine/procedures" + "my5G-RANTester/internal/scenario" "os" "os/signal" "sync" @@ -113,3 +114,70 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb time.Sleep(time.Second * 1) } + +func TestSingleUe(tunnelMode config.TunnelMode, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, numPduSessions int) { + if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { + log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") + } + + if numPduSessions > 16 { + log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") + } + + cfg := config.GetConfig() + + tasks := []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{cfg.GNodeB.PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + } + + for i := 0; i < numPduSessions; i++ { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NewPDUSession, + }) + } + + if timeBeforeNgapHandover != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NGAPHandover, + Delay: timeBeforeNgapHandover, + }) + } + + if timeBeforeXnHandover != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.XNHandover, + Delay: timeBeforeXnHandover, + }) + } + if timeBeforeIdle != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Idle, + Delay: timeBeforeIdle, + }) + } + + if timeBeforeDeregistration != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Deregistration, + Delay: timeBeforeDeregistration, + }) + } + + gnbs := []config.GNodeB{cfg.GNodeB} + ueScenario := scenario.UEScenario{ + Config: cfg.Ue, + Tasks: tasks, + } + ueScenarios := []scenario.UEScenario{ueScenario} + + r := scenario.Runner{} + r.Start(gnbs, cfg.AMF, ueScenarios, 0) +} diff --git a/test/pr_test.go b/test/pr_test.go index fad2c933..0cf02fb8 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -9,6 +9,7 @@ import ( "my5G-RANTester/config" "my5G-RANTester/internal/common/tools" "my5G-RANTester/internal/control_test_engine/procedures" + "my5G-RANTester/internal/scenario" "my5G-RANTester/test/aio5gc" "my5G-RANTester/test/aio5gc/context" amfTools "my5G-RANTester/test/aio5gc/lib/tools" @@ -23,6 +24,71 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSingleUe(t *testing.T) { + controlIFConfig := config.ControlIF{ + Ip: "127.0.0.1", + Port: 9489, + } + dataIFConfig := config.DataIF{ + Ip: "127.0.0.1", + Port: 2154, + } + amfConfig := config.AMF{ + Ip: "127.0.0.1", + Port: 38414, + } + + conf := amfTools.GenerateDefaultConf(controlIFConfig, dataIFConfig, amfConfig) + + // Setup 5GC + builder := aio5gc.FiveGCBuilder{} + fiveGC, err := builder. + WithConfig(conf). + Build() + if err != nil { + log.Printf("[5GC] Error during 5GC creation %v", err) + os.Exit(1) + } + time.Sleep(1 * time.Second) + + securityContext := context.SecurityContext{} + securityContext.SetMsin(conf.Ue.Msin) + securityContext.SetAuthSubscription(conf.Ue.Key, conf.Ue.Opc, "c9e8763286b5b9ffbdf56e1297d0887b", conf.Ue.Amf, conf.Ue.Sqn) + securityContext.SetAbba([]uint8{0x00, 0x00}) + + amfContext := fiveGC.GetAMFContext() + amfContext.Provision(models.Snssai{Sst: int32(conf.Ue.Snssai.Sst), Sd: conf.Ue.Snssai.Sd}, securityContext) + + gnbs := []config.GNodeB{conf.GNodeB} + ueScenario := scenario.UEScenario{ + Config: conf.Ue, + Tasks: []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{conf.GNodeB.PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + { + TaskType: scenario.NewPDUSession, + }, + { + TaskType: scenario.Deregistration, + Delay: 10000, + }, + }, + } + ueScenarios := []scenario.UEScenario{ueScenario} + + r := scenario.Runner{} + r.Start(gnbs, conf.AMF, ueScenarios, 0) + + assert.True(t, true) +} + func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { controlIFConfig := config.ControlIF{ @@ -136,7 +202,7 @@ func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { }) }) assert.Equalf(t, ueCount, i, "Expected %v ue to created in 5GC state but was %v", ueCount, i) - + } func TestUERegistrationLoop(t *testing.T) { From 8064c157bbf60cfc243107f467e1ca3342bd319a Mon Sep 17 00:00:00 2001 From: Raguideau Date: Wed, 20 Mar 2024 12:59:49 +0000 Subject: [PATCH 2/9] add loop and handover Signed-off-by: Raguideau --- internal/scenario/scenario.go | 274 +++++++++++++----- internal/scenario/task.go | 27 ++ internal/templates/test-multi-ues-in-queue.go | 68 ----- internal/templates/test-single-ue.go | 122 ++++++++ .../handler/ueOriginatingDeregistration.go | 1 - test/pr_test.go | 31 +- 6 files changed, 381 insertions(+), 142 deletions(-) create mode 100644 internal/templates/test-single-ue.go diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index 4134f264..e5364230 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -22,21 +22,27 @@ import ( log "github.com/sirupsen/logrus" ) -type Runner struct { +type ScenarioManager struct { gnbs map[string]*context.GNBContext - ues map[int]*ueSim + ues map[int]*ueScenarioCtx } -type ueSim struct { +// Description of the scenario to be run for one UE +type UEScenario struct { + Config config.Ue + Tasks []Task + Loop int // Time between two loops + ForceStop int // Time before forcefully stoping scenario (0 to desactivate) +} + +// Context of running ue scenario +type ueScenarioCtx struct { ongoingTask bool nextTasks []Task taskChan chan Task reportChan chan report -} - -type UEScenario struct { - Config config.Ue - Tasks []Task + loopTicker *time.Ticker + endTicker *time.Ticker } type report struct { @@ -44,7 +50,7 @@ type report struct { reason string } -func (s *Runner) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario, maxRequestRate int) { +func (s *ScenarioManager) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario, maxRequestRate int) { wg := sync.WaitGroup{} @@ -59,7 +65,7 @@ func (s *Runner) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios // TODO: We should wait for NGSetupResponse instead time.Sleep(2 * time.Second) - s.ues = make(map[int]*ueSim) + s.ues = make(map[int]*ueScenarioCtx) sigStop := make(chan os.Signal, 1) signal.Notify(sigStop, os.Interrupt) @@ -68,14 +74,29 @@ func (s *Runner) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios done := true for ueId := 1; !stopSignal && ueId <= len(ueScenarios); ueId++ { if s.ues[ueId] == nil { - ue := ueSim{ + ue := ueScenarioCtx{ ongoingTask: false, nextTasks: ueScenarios[ueId-1].Tasks, taskChan: make(chan Task, 1), reportChan: make(chan report, 1), } - go simulateSingleUE(ueId, ueScenarios[ueId-1].Config, ue.taskChan, ue.reportChan, s.gnbs, &wg) + if ueScenarios[ueId-1].Loop > 0 { + ue.loopTicker = time.NewTicker(time.Duration(ueScenarios[ueId-1].Loop) * time.Millisecond) + } + if ueScenarios[ueId-1].ForceStop > 0 { + ue.endTicker = time.NewTicker(time.Duration(ueScenarios[ueId-1].ForceStop) * time.Millisecond) + } + + executer := ueTaskExecuter{ + UeId: ueId, + UeCfg: ueScenarios[ueId-1].Config, + TaskChan: ue.taskChan, + ReportChan: ue.reportChan, + Gnbs: s.gnbs, + Wg: &wg, + } + executer.Run() s.ues[ueId] = &ue } @@ -85,12 +106,34 @@ func (s *Runner) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios if !report.success { s.ues[ueId].nextTasks = []Task{} + log.Debug("------------------------------------------- Got report: " + report.reason + " ------------------------------------------- ") } s.ues[ueId].ongoingTask = false case <-sigStop: stopSignal = true default: } + if ueScenarios[ueId-1].ForceStop > 0 { + select { + case <-s.ues[ueId].endTicker.C: + done = true + log.Debugf("[UE-%d]Force end of scenario", ueId) + continue + default: + } + } + if ueScenarios[ueId-1].Loop > 0 { + done = false + select { + case <-s.ues[ueId].loopTicker.C: + log.Debugf("[UE-%d] starting new loop", ueId) + s.ues[ueId].taskChan <- Task{TaskType: Kill} + s.ues[ueId].nextTasks = ueScenarios[ueId-1].Tasks + s.ues[ueId].ongoingTask = true + continue + default: + } + } if s.ues[ueId].ongoingTask { done = false @@ -114,110 +157,205 @@ func (s *Runner) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios } } - time.Sleep(time.Second * 1) + time.Sleep(time.Second * 3) } -func simulateSingleUE(ueId int, ueCfg config.Ue, taskChan chan Task, reportChan chan report, gnbs map[string]*context.GNBContext, wg *sync.WaitGroup) { - log.Infof("simulation started for ue %d", ueId) +type ueTaskExecuter struct { + UeId int + UeCfg config.Ue + TaskChan chan Task + ReportChan chan report + Gnbs map[string]*context.GNBContext + Wg *sync.WaitGroup + + running bool + ueRx chan procedures.UeTesterMessage + ueTx chan scenario.ScenarioMessage + attachedGnb string + timedTasks map[Task]*time.Timer + state int + targetState int + lock sync.Mutex + toBeKilled chan procedures.UeTesterMessage +} - wg.Add(1) +func (e *ueTaskExecuter) Run() { + if e.running { + log.Errorf("simulation for ue %d failed: already running", e.UeId) + return + } + if e.UeCfg == (config.Ue{}) { + log.Errorf("simulation for ue %d failed: no config", e.UeId) + return + } + if e.TaskChan == nil { + log.Errorf("simulation for ue %d failed: no taskChan", e.UeId) + return + } + if e.ReportChan == nil { + log.Errorf("simulation for ue %d failed: no reportChan", e.UeId) + return + } + if e.Gnbs == nil { + log.Errorf("simulation for ue %d failed: no gnbs", e.UeId) + return + } + if e.Wg == nil { + log.Errorf("simulation for ue %d failed: no WaitGroup is given", e.UeId) + return + } - ueRx := make(chan procedures.UeTesterMessage) - var ueTx chan scenario.ScenarioMessage + e.running = true + e.timedTasks = map[Task]*time.Timer{} + e.state = ueCtx.MM5G_NULL + e.targetState = ueCtx.MM5G_NULL + go e.listen() +} +func (e *ueTaskExecuter) listen() { + log.Infof("simulation started for ue %d", e.UeId) loop := true - var attachedGnb string - state := ueCtx.MM5G_NULL - targetState := ueCtx.MM5G_NULL for loop { select { - case task := <-taskChan: - if attachedGnb == "" && task.TaskType != AttachToGNB { - msg := fmt.Sprintf("UE %d is not attached to a GNB", ueId) + case task := <-e.TaskChan: + log.Debugf("------------------------------------------- received task: %s ------------------------------------------- ", task.TaskType.ToStr()) + if e.attachedGnb == "" && task.TaskType != AttachToGNB { + msg := fmt.Sprintf("UE %d is not attached to a GNB", e.UeId) log.Error(msg) - reportChan <- report{success: false, reason: msg} + e.ReportChan <- report{success: false, reason: msg} } switch task.TaskType { case AttachToGNB: - if gnbs[task.Parameters.GnbId] != nil { + if e.Gnbs[task.Parameters.GnbId] != nil { // Create a new UE coroutine // ue.NewUE returns context of the new UE - ueTx = ue.NewUE(ueCfg, ueId, ueRx, gnbs[task.Parameters.GnbId].GetInboundChannel(), wg) - attachedGnb = task.Parameters.GnbId - reportChan <- report{success: true} + e.lock.Lock() + e.ueRx = make(chan procedures.UeTesterMessage) + e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.Wg) + e.Wg.Add(1) + e.attachedGnb = task.Parameters.GnbId + e.lock.Unlock() + e.ReportChan <- report{success: true} } else { msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) log.Error(msg) - reportChan <- report{success: false, reason: msg} + e.ReportChan <- report{success: false, reason: msg} } case Registration: - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} }) - targetState = ueCtx.MM5G_REGISTERED + if e.toBeKilled != nil { + e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Kill} + } + e.sendProcedureToUE(task, procedures.Registration) + e.targetState = ueCtx.MM5G_REGISTERED case Deregistration: - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.Deregistration} }) - targetState = ueCtx.MM5G_DEREGISTERED + e.sendProcedureToUE(task, procedures.Deregistration) + e.targetState = ueCtx.MM5G_DEREGISTERED case Idle: - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.Idle} }) - targetState = ueCtx.MM5G_IDLE + e.sendProcedureToUE(task, procedures.Idle) + e.targetState = ueCtx.MM5G_IDLE case NewPDUSession: - if state == ueCtx.MM5G_REGISTERED { // TODO: check if it's possible to send pdu req without being registrated (for negative tests) - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} }) + if e.state == ueCtx.MM5G_REGISTERED { + e.sendProcedureToUE(task, procedures.NewPDUSession) if !task.Hang { - reportChan <- report{success: true, reason: "Need to implement way to check if pduSession is successful"} + // TODO: Implement way to check if pduSession is successful + e.ReportChan <- report{success: true, reason: "sent PDU session request"} } } else { - msg := fmt.Sprintf("UE %d is not registered", ueId) + msg := fmt.Sprintf("UE %d is not registered", e.UeId) log.Error(msg) - reportChan <- report{success: false, reason: msg} + e.ReportChan <- report{success: false, reason: msg} } case XNHandover: - if gnbs[task.Parameters.GnbId] != nil { + if e.Gnbs[task.Parameters.GnbId] != nil { time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - trigger.TriggerXnHandover(gnbs[attachedGnb], gnbs[task.Parameters.GnbId], int64(ueId)) + trigger.TriggerXnHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) + e.attachedGnb = task.Parameters.GnbId + // Wait for succesful handover + // TODO: We should wait for confimation from gnb instead of timer + time.Sleep(2 * time.Second) + e.ReportChan <- report{success: true, reason: "succesful XNHandover"} }) - targetState = ueCtx.MM5G_REGISTERED + e.targetState = ueCtx.MM5G_REGISTERED } else { msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) log.Error(msg) - reportChan <- report{success: false, reason: msg} + e.ReportChan <- report{success: false, reason: msg} } case NGAPHandover: - if gnbs[task.Parameters.GnbId] != nil { + if e.Gnbs[task.Parameters.GnbId] != nil { time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - trigger.TriggerNgapHandover(gnbs[attachedGnb], gnbs[task.Parameters.GnbId], int64(ueId)) + trigger.TriggerNgapHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) + e.attachedGnb = task.Parameters.GnbId + // Wait for succesful handover + // TODO: We should wait for confimation from gnb instead of timer + time.Sleep(2 * time.Second) + e.ReportChan <- report{success: true, reason: "succesful NGAPHandover"} }) - targetState = ueCtx.MM5G_REGISTERED + e.targetState = ueCtx.MM5G_REGISTERED } else { msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) log.Error(msg) - reportChan <- report{success: false, reason: msg} + e.ReportChan <- report{success: false, reason: msg} } case Terminate: - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - ueRx <- procedures.UeTesterMessage{Type: procedures. - Terminate} - ueRx = nil + e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + e.lock.Lock() + defer e.lock.Unlock() + if e.toBeKilled != nil { + e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Terminate} + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} + e.ueRx = nil + e.ueTx = nil + e.state = ueCtx.MM5G_NULL + e.attachedGnb = "" + for t := range e.timedTasks { + e.timedTasks[t].Stop() + delete(e.timedTasks, t) + } + e.ReportChan <- report{success: true, reason: "UE Terminated"} }) case Kill: - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - ueRx <- procedures.UeTesterMessage{Type: procedures. - Kill} - ueRx = nil - state = ueCtx.MM5G_NULL - attachedGnb = "" + e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + e.lock.Lock() + defer e.lock.Unlock() + e.toBeKilled = e.ueRx + e.ueRx = nil + e.ueTx = nil + e.state = ueCtx.MM5G_NULL + e.attachedGnb = "" + for t := range e.timedTasks { + e.timedTasks[t].Stop() + delete(e.timedTasks, t) + } + e.ReportChan <- report{success: true, reason: "UE Killed"} }) } - case ueMsg := <-ueTx: - msg := fmt.Sprint("[UE] Switched from state ", state, " to state ", ueMsg.StateChange) - log.Info(msg) - if ueMsg.StateChange == targetState { - reportChan <- report{success: true, reason: msg} - } - if ueMsg.StateChange == ueCtx.MM5G_NULL { - loop = false + case ueMsg, open := <-e.ueTx: + if open { + msg := fmt.Sprintf("[UE-%d] Switched from state %d to state %d ", e.UeId, e.state, ueMsg.StateChange) + log.Info(msg) + if ueMsg.StateChange == e.targetState { + e.ReportChan <- report{success: true, reason: msg} + } + e.state = ueMsg.StateChange } - state = ueMsg.StateChange } } + e.running = false +} + +func (e *ueTaskExecuter) sendProcedureToUE(task Task, procedure procedures.UeTesterMessageType) { + if task.Delay > 0 { + e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + e.ueRx <- procedures.UeTesterMessage{Type: procedure} + e.lock.Lock() + e.timedTasks[task].Stop() + delete(e.timedTasks, task) + e.lock.Unlock() + }) + } else { + e.ueRx <- procedures.UeTesterMessage{Type: procedure} + } } diff --git a/internal/scenario/task.go b/internal/scenario/task.go index 3fb3cb72..84b55f43 100644 --- a/internal/scenario/task.go +++ b/internal/scenario/task.go @@ -27,3 +27,30 @@ const ( NGAPHandover XNHandover ) + +func (t TaskType) ToStr() string { + switch t { + case AttachToGNB: + return "AttachToGNB" + case Registration: + return "Registration" + case Deregistration: + return "Deregistration" + case NewPDUSession: + return "NewPDUSession" + // case DestroyPDUSession: + // return "" + case Terminate: + return "Terminate" + case Kill: + return "Kill" + case Idle: + return "Idle" + case NGAPHandover: + return "NGAPHandover" + case XNHandover: + return "XNHandover" + default: + return "Undefined" + } +} diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index a7d0d7aa..7e5d99c9 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -8,7 +8,6 @@ import ( "my5G-RANTester/config" "my5G-RANTester/internal/common/tools" "my5G-RANTester/internal/control_test_engine/procedures" - "my5G-RANTester/internal/scenario" "os" "os/signal" "sync" @@ -114,70 +113,3 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb time.Sleep(time.Second * 1) } - -func TestSingleUe(tunnelMode config.TunnelMode, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, numPduSessions int) { - if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { - log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") - } - - if numPduSessions > 16 { - log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") - } - - cfg := config.GetConfig() - - tasks := []scenario.Task{ - { - TaskType: scenario.AttachToGNB, - Parameters: struct { - GnbId string - }{cfg.GNodeB.PlmnList.GnbId}, - }, - { - TaskType: scenario.Registration, - }, - } - - for i := 0; i < numPduSessions; i++ { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.NewPDUSession, - }) - } - - if timeBeforeNgapHandover != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.NGAPHandover, - Delay: timeBeforeNgapHandover, - }) - } - - if timeBeforeXnHandover != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.XNHandover, - Delay: timeBeforeXnHandover, - }) - } - if timeBeforeIdle != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.Idle, - Delay: timeBeforeIdle, - }) - } - - if timeBeforeDeregistration != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.Deregistration, - Delay: timeBeforeDeregistration, - }) - } - - gnbs := []config.GNodeB{cfg.GNodeB} - ueScenario := scenario.UEScenario{ - Config: cfg.Ue, - Tasks: tasks, - } - ueScenarios := []scenario.UEScenario{ueScenario} - - r := scenario.Runner{} - r.Start(gnbs, cfg.AMF, ueScenarios, 0) -} diff --git a/internal/templates/test-single-ue.go b/internal/templates/test-single-ue.go new file mode 100644 index 00000000..5f9b30aa --- /dev/null +++ b/internal/templates/test-single-ue.go @@ -0,0 +1,122 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * © Copyright 2023 Hewlett Packard Enterprise Development LP + */ +package templates + +import ( + "fmt" + "my5G-RANTester/config" + "my5G-RANTester/internal/common/tools" + "my5G-RANTester/internal/scenario" + "strconv" + + log "github.com/sirupsen/logrus" +) + +func TestSingleUe(tunnelMode config.TunnelMode, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, numPduSessions int) { + if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { + log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") + } + + if numPduSessions > 16 { + log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") + } + + cfg := config.GetConfig() + + var err error + gnb2 := cfg.GNodeB + gnb2.PlmnList.GnbId = genereateGnbId(1, cfg.GNodeB.PlmnList.GnbId) + + gnb2.ControlIF.Ip, err = tools.IncrementIP(cfg.GNodeB.ControlIF.Ip, "0.0.0.0/0") + if err != nil { + log.Fatal("[GNB][CONFIG] Error while allocating ip for N2: " + err.Error()) + } + gnb2.DataIF.Ip, err = tools.IncrementIP(cfg.GNodeB.DataIF.Ip, "0.0.0.0/0") + if err != nil { + log.Fatal("[GNB][CONFIG] Error while allocating ip for N3: " + err.Error()) + } + + gnbs := []config.GNodeB{cfg.GNodeB, gnb2} + nextgnb := 0 + + tasks := []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{gnbs[nextgnb].PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + } + + for i := 0; i < numPduSessions; i++ { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NewPDUSession, + }) + } + + // TODO: use arg position to determine order of procedures + if timeBeforeNgapHandover != 0 { + nextgnb = (nextgnb + 1) % 2 + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NGAPHandover, + Delay: timeBeforeNgapHandover, + Parameters: struct { + GnbId string + }{gnbs[nextgnb].PlmnList.GnbId}, + }) + } + + if timeBeforeXnHandover != 0 { + nextgnb = (nextgnb + 1) % 2 + tasks = append(tasks, scenario.Task{ + TaskType: scenario.XNHandover, + Delay: timeBeforeXnHandover, + Parameters: struct { + GnbId string + }{gnbs[nextgnb].PlmnList.GnbId}, + }) + } + if timeBeforeIdle != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Idle, + Delay: timeBeforeIdle, + }) + } + + if timeBeforeDeregistration != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Deregistration, + Delay: timeBeforeDeregistration, + }) + } + + ueScenario := scenario.UEScenario{ + Config: cfg.Ue, + Tasks: tasks, + } + ueScenarios := []scenario.UEScenario{ueScenario} + + if loop { + ueScenario.Loop = timeBetweenRegistration + } + + r := scenario.ScenarioManager{} + r.Start(gnbs, cfg.AMF, ueScenarios, 0) +} + +func genereateGnbId(i int, gnbId string) string { + + gnbId_int, err := strconv.ParseInt(gnbId, 16, 0) + if err != nil { + log.Fatal("[UE][CONFIG] Given gnbId is invalid") + } + base := int(gnbId_int) + i + + gnbId = fmt.Sprintf("%06x", base) + return gnbId +} diff --git a/test/aio5gc/msg/nas/handler/ueOriginatingDeregistration.go b/test/aio5gc/msg/nas/handler/ueOriginatingDeregistration.go index 4126d04a..4485ef7d 100644 --- a/test/aio5gc/msg/nas/handler/ueOriginatingDeregistration.go +++ b/test/aio5gc/msg/nas/handler/ueOriginatingDeregistration.go @@ -38,7 +38,6 @@ func DefaultUEOriginatingDeregistration(nasReq *nas.Message, amf *context.AMFCon context.ForceReleaseAllPDUSession(ue) err := ue.GetUeFsm().SendEvent(ue.GetState(), context.DeregistrationRequest, fsm.ArgsType{"ue": ue}, log.NewEntry(log.StandardLogger())) - if err != nil { return err } diff --git a/test/pr_test.go b/test/pr_test.go index 0cf02fb8..679b8e1e 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -39,11 +39,24 @@ func TestSingleUe(t *testing.T) { } conf := amfTools.GenerateDefaultConf(controlIFConfig, dataIFConfig, amfConfig) + type UECheck struct { + HasAuthOnce bool + } + ueChecks := map[string]*UECheck{} // Setup 5GC builder := aio5gc.FiveGCBuilder{} fiveGC, err := builder. WithConfig(conf). + WithUeCallback(context.Authenticated, func(state *fsm.State, event fsm.EventType, args fsm.ArgsType) { + ue := args["ue"].(*context.UEContext) + check, ok := ueChecks[ue.GetSecurityContext().GetMsin()] + if !ok { + check = &UECheck{} + ueChecks[ue.GetSecurityContext().GetMsin()] = check + } + check.HasAuthOnce = true + }). Build() if err != nil { log.Printf("[5GC] Error during 5GC creation %v", err) @@ -61,7 +74,9 @@ func TestSingleUe(t *testing.T) { gnbs := []config.GNodeB{conf.GNodeB} ueScenario := scenario.UEScenario{ - Config: conf.Ue, + Config: conf.Ue, + Loop: 500, + ForceStop: 20000, Tasks: []scenario.Task{ { TaskType: scenario.AttachToGNB, @@ -77,16 +92,22 @@ func TestSingleUe(t *testing.T) { }, { TaskType: scenario.Deregistration, - Delay: 10000, + Delay: 2000, }, }, } ueScenarios := []scenario.UEScenario{ueScenario} - r := scenario.Runner{} + r := scenario.ScenarioManager{} r.Start(gnbs, conf.AMF, ueScenarios, 0) - assert.True(t, true) + time.Sleep(time.Duration(1000) * time.Millisecond) + + fiveGC.GetAMFContext().ExecuteForAllUe( + func(ue *context.UEContext) { + assert.Equalf(t, context.Deregistered, ue.GetState().Current(), "Expected all ue to be in Deregistered state but was not") + assert.True(t, ueChecks[ue.GetSecurityContext().GetMsin()].HasAuthOnce, "UE has never changed state") + }) } func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { @@ -202,7 +223,7 @@ func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { }) }) assert.Equalf(t, ueCount, i, "Expected %v ue to created in 5GC state but was %v", ueCount, i) - + } func TestUERegistrationLoop(t *testing.T) { From 76aa08d3eabd61b486c749578d87156bd68e0a4a Mon Sep 17 00:00:00 2001 From: Raguideau Date: Thu, 21 Mar 2024 16:16:26 +0000 Subject: [PATCH 3/9] manage procedure order for single ue test Signed-off-by: Raguideau --- cmd/packetrusher.go | 3 +- internal/scenario/scenario.go | 27 ++-- internal/templates/test-custom-scenario.go | 2 +- internal/templates/test-multi-ues-in-queue.go | 117 +++++++++++++++++ internal/templates/test-single-ue.go | 122 ------------------ 5 files changed, 138 insertions(+), 133 deletions(-) delete mode 100644 internal/templates/test-single-ue.go diff --git a/cmd/packetrusher.go b/cmd/packetrusher.go index 43f72d71..82341872 100644 --- a/cmd/packetrusher.go +++ b/cmd/packetrusher.go @@ -138,7 +138,8 @@ func main() { tunnelMode = config.TunnelTun } } - templates.TestMultiUesInQueue(numUes, tunnelMode, c.Bool("dedicatedGnb"), c.Bool("loop"), c.Int("timeBetweenRegistration"), c.Int("timeBeforeDeregistration"), c.Int("timeBeforeNgapHandover"), c.Int("timeBeforeXnHandover"), c.Int("timeBeforeIdle"), c.Int("timeBeforeReconnecting"), c.Int("numPduSessions")) + // templates.TestMultiUesInQueue(numUes, tunnelMode, c.Bool("dedicatedGnb"), c.Bool("loop"), c.Int("timeBetweenRegistration"), c.Int("timeBeforeDeregistration"), c.Int("timeBeforeNgapHandover"), c.Int("timeBeforeXnHandover"), c.Int("timeBeforeIdle"), c.Int("timeBeforeReconnecting"), c.Int("numPduSessions")) + templates.TestsingleUePdu(tunnelMode, c.Bool("loop"), c.Int("timeBetweenRegistration"), c.Int("timeBeforeDeregistration"), c.Int("timeBeforeNgapHandover"), c.Int("timeBeforeXnHandover"), c.Int("timeBeforeIdle"), c.Int("numPduSessions")) return nil }, diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index e5364230..e76d3bd2 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -168,15 +168,16 @@ type ueTaskExecuter struct { Gnbs map[string]*context.GNBContext Wg *sync.WaitGroup - running bool - ueRx chan procedures.UeTesterMessage - ueTx chan scenario.ScenarioMessage - attachedGnb string - timedTasks map[Task]*time.Timer - state int - targetState int - lock sync.Mutex - toBeKilled chan procedures.UeTesterMessage + running bool + ueRx chan procedures.UeTesterMessage + ueTx chan scenario.ScenarioMessage + attachedGnb string + timedTasks map[Task]*time.Timer + lastReceivedTaskTime time.Time + state int + targetState int + lock sync.Mutex + toBeKilled chan procedures.UeTesterMessage } func (e *ueTaskExecuter) Run() { @@ -209,6 +210,7 @@ func (e *ueTaskExecuter) Run() { e.timedTasks = map[Task]*time.Timer{} e.state = ueCtx.MM5G_NULL e.targetState = ueCtx.MM5G_NULL + e.lastReceivedTaskTime = time.Now() go e.listen() } @@ -224,6 +226,12 @@ func (e *ueTaskExecuter) listen() { log.Error(msg) e.ReportChan <- report{success: false, reason: msg} } + if task.Delay > 0 { + since := time.Since(e.lastReceivedTaskTime).Milliseconds() + log.Info(since) + task.Delay = max(0, task.Delay-int(time.Since(e.lastReceivedTaskTime).Milliseconds())) + } + e.lastReceivedTaskTime = time.Now() switch task.TaskType { case AttachToGNB: if e.Gnbs[task.Parameters.GnbId] != nil { @@ -258,6 +266,7 @@ func (e *ueTaskExecuter) listen() { e.sendProcedureToUE(task, procedures.NewPDUSession) if !task.Hang { // TODO: Implement way to check if pduSession is successful + time.Sleep(2 * time.Second) e.ReportChan <- report{success: true, reason: "sent PDU session request"} } } else { diff --git a/internal/templates/test-custom-scenario.go b/internal/templates/test-custom-scenario.go index f4c1be82..586aa5d7 100644 --- a/internal/templates/test-custom-scenario.go +++ b/internal/templates/test-custom-scenario.go @@ -81,7 +81,7 @@ func TestWithCustomScenario(scenarioPath string) { module, err := runtime.InstantiateWithConfig(ctx, addWasm, config) if err != nil { - log.Fatal("failed to instantiate module: %v", err) + log.Fatalf("failed to instantiate module: %v", err) } // Call the `add` function and print the results to the console. diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index 7e5d99c9..2af45695 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -5,11 +5,15 @@ package templates import ( + "fmt" "my5G-RANTester/config" "my5G-RANTester/internal/common/tools" "my5G-RANTester/internal/control_test_engine/procedures" + "my5G-RANTester/internal/scenario" "os" "os/signal" + "sort" + "strconv" "sync" "time" @@ -113,3 +117,116 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb time.Sleep(time.Second * 1) } + +func TestsingleUePdu(tunnelMode config.TunnelMode, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, numPduSessions int) { + if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { + log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") + } + + if numPduSessions > 16 { + log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") + } + + cfg := config.GetConfig() + + var err error + gnb2 := cfg.GNodeB + gnb2.PlmnList.GnbId = genereateGnbId(1, cfg.GNodeB.PlmnList.GnbId) + + gnb2.ControlIF.Ip, err = tools.IncrementIP(cfg.GNodeB.ControlIF.Ip, "0.0.0.0/0") + if err != nil { + log.Fatal("[GNB][CONFIG] Error while allocating ip for N2: " + err.Error()) + } + gnb2.DataIF.Ip, err = tools.IncrementIP(cfg.GNodeB.DataIF.Ip, "0.0.0.0/0") + if err != nil { + log.Fatal("[GNB][CONFIG] Error while allocating ip for N3: " + err.Error()) + } + + gnbs := []config.GNodeB{cfg.GNodeB, gnb2} + nextgnb := 0 + + tasks := []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{gnbs[nextgnb].PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + } + + for i := 0; i < numPduSessions; i++ { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NewPDUSession, + }) + } + + if timeBeforeNgapHandover != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NGAPHandover, + Delay: timeBeforeNgapHandover, + }) + } + + if timeBeforeXnHandover != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.XNHandover, + Delay: timeBeforeXnHandover, + }) + } + if timeBeforeIdle != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Idle, + Delay: timeBeforeIdle, + }) + } + + if timeBeforeDeregistration != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Deregistration, + Delay: timeBeforeDeregistration, + }) + } + + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].Delay < tasks[j].Delay + }) + + sumDelay := 0 + for i := 0; i < len(tasks); i++ { + tasks[i].Delay = tasks[i].Delay - sumDelay + sumDelay += tasks[i].Delay + + if tasks[i].TaskType == scenario.NGAPHandover || tasks[i].TaskType == scenario.XNHandover { + nextgnb = (nextgnb + 1) % 2 + tasks[i].Parameters.GnbId = gnbs[nextgnb].PlmnList.GnbId + } + } + + ueScenario := scenario.UEScenario{ + Config: cfg.Ue, + Tasks: tasks, + } + ueScenarios := []scenario.UEScenario{ueScenario} + + if loop { + ueScenario.Loop = timeBetweenRegistration + } + + r := scenario.ScenarioManager{} + r.Start(gnbs, cfg.AMF, ueScenarios, 0) +} + +func genereateGnbId(i int, gnbId string) string { + + gnbId_int, err := strconv.ParseInt(gnbId, 16, 0) + if err != nil { + log.Fatal("[UE][CONFIG] Given gnbId is invalid") + } + base := int(gnbId_int) + i + + gnbId = fmt.Sprintf("%06x", base) + return gnbId +} diff --git a/internal/templates/test-single-ue.go b/internal/templates/test-single-ue.go deleted file mode 100644 index 5f9b30aa..00000000 --- a/internal/templates/test-single-ue.go +++ /dev/null @@ -1,122 +0,0 @@ -/** - * SPDX-License-Identifier: Apache-2.0 - * © Copyright 2023 Hewlett Packard Enterprise Development LP - */ -package templates - -import ( - "fmt" - "my5G-RANTester/config" - "my5G-RANTester/internal/common/tools" - "my5G-RANTester/internal/scenario" - "strconv" - - log "github.com/sirupsen/logrus" -) - -func TestSingleUe(tunnelMode config.TunnelMode, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, numPduSessions int) { - if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { - log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") - } - - if numPduSessions > 16 { - log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") - } - - cfg := config.GetConfig() - - var err error - gnb2 := cfg.GNodeB - gnb2.PlmnList.GnbId = genereateGnbId(1, cfg.GNodeB.PlmnList.GnbId) - - gnb2.ControlIF.Ip, err = tools.IncrementIP(cfg.GNodeB.ControlIF.Ip, "0.0.0.0/0") - if err != nil { - log.Fatal("[GNB][CONFIG] Error while allocating ip for N2: " + err.Error()) - } - gnb2.DataIF.Ip, err = tools.IncrementIP(cfg.GNodeB.DataIF.Ip, "0.0.0.0/0") - if err != nil { - log.Fatal("[GNB][CONFIG] Error while allocating ip for N3: " + err.Error()) - } - - gnbs := []config.GNodeB{cfg.GNodeB, gnb2} - nextgnb := 0 - - tasks := []scenario.Task{ - { - TaskType: scenario.AttachToGNB, - Parameters: struct { - GnbId string - }{gnbs[nextgnb].PlmnList.GnbId}, - }, - { - TaskType: scenario.Registration, - }, - } - - for i := 0; i < numPduSessions; i++ { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.NewPDUSession, - }) - } - - // TODO: use arg position to determine order of procedures - if timeBeforeNgapHandover != 0 { - nextgnb = (nextgnb + 1) % 2 - tasks = append(tasks, scenario.Task{ - TaskType: scenario.NGAPHandover, - Delay: timeBeforeNgapHandover, - Parameters: struct { - GnbId string - }{gnbs[nextgnb].PlmnList.GnbId}, - }) - } - - if timeBeforeXnHandover != 0 { - nextgnb = (nextgnb + 1) % 2 - tasks = append(tasks, scenario.Task{ - TaskType: scenario.XNHandover, - Delay: timeBeforeXnHandover, - Parameters: struct { - GnbId string - }{gnbs[nextgnb].PlmnList.GnbId}, - }) - } - if timeBeforeIdle != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.Idle, - Delay: timeBeforeIdle, - }) - } - - if timeBeforeDeregistration != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.Deregistration, - Delay: timeBeforeDeregistration, - }) - } - - ueScenario := scenario.UEScenario{ - Config: cfg.Ue, - Tasks: tasks, - } - ueScenarios := []scenario.UEScenario{ueScenario} - - if loop { - ueScenario.Loop = timeBetweenRegistration - } - - r := scenario.ScenarioManager{} - r.Start(gnbs, cfg.AMF, ueScenarios, 0) -} - -func genereateGnbId(i int, gnbId string) string { - - gnbId_int, err := strconv.ParseInt(gnbId, 16, 0) - if err != nil { - log.Fatal("[UE][CONFIG] Given gnbId is invalid") - } - base := int(gnbId_int) + i - - gnbId = fmt.Sprintf("%06x", base) - return gnbId -} From 1a707873ce0301294bb8518bd2fab46a7a3c8757 Mon Sep 17 00:00:00 2001 From: Raguideau Date: Mon, 25 Mar 2024 09:03:39 +0000 Subject: [PATCH 4/9] add multiUePdu scenario creation && rework scenario executer Signed-off-by: Raguideau --- cmd/packetrusher.go | 3 +- internal/scenario/scenario.go | 288 ++++++++++-------- internal/scenario/task.go | 3 + internal/templates/test-multi-ues-in-queue.go | 266 ++++++---------- test/pr_test.go | 2 +- 5 files changed, 254 insertions(+), 308 deletions(-) diff --git a/cmd/packetrusher.go b/cmd/packetrusher.go index 82341872..43f72d71 100644 --- a/cmd/packetrusher.go +++ b/cmd/packetrusher.go @@ -138,8 +138,7 @@ func main() { tunnelMode = config.TunnelTun } } - // templates.TestMultiUesInQueue(numUes, tunnelMode, c.Bool("dedicatedGnb"), c.Bool("loop"), c.Int("timeBetweenRegistration"), c.Int("timeBeforeDeregistration"), c.Int("timeBeforeNgapHandover"), c.Int("timeBeforeXnHandover"), c.Int("timeBeforeIdle"), c.Int("timeBeforeReconnecting"), c.Int("numPduSessions")) - templates.TestsingleUePdu(tunnelMode, c.Bool("loop"), c.Int("timeBetweenRegistration"), c.Int("timeBeforeDeregistration"), c.Int("timeBeforeNgapHandover"), c.Int("timeBeforeXnHandover"), c.Int("timeBeforeIdle"), c.Int("numPduSessions")) + templates.TestMultiUesInQueue(numUes, tunnelMode, c.Bool("dedicatedGnb"), c.Bool("loop"), c.Int("timeBetweenRegistration"), c.Int("timeBeforeDeregistration"), c.Int("timeBeforeNgapHandover"), c.Int("timeBeforeXnHandover"), c.Int("timeBeforeIdle"), c.Int("timeBeforeReconnecting"), c.Int("numPduSessions")) return nil }, diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index e76d3bd2..988ac7f2 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -50,7 +50,7 @@ type report struct { reason string } -func (s *ScenarioManager) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario, maxRequestRate int) { +func (s *ScenarioManager) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario) { wg := sync.WaitGroup{} @@ -103,10 +103,12 @@ func (s *ScenarioManager) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ue select { case report := <-s.ues[ueId].reportChan: - + log.Debugf("------------------------------------ Report UE-%d ------------------------------------", ueId) if !report.success { s.ues[ueId].nextTasks = []Task{} - log.Debug("------------------------------------------- Got report: " + report.reason + " ------------------------------------------- ") + log.Errorf("[UE-%d] Reported error: %s", ueId, report.reason) + } else { + log.Infof("[UE-%d] Reported succes: %s", ueId, report.reason) } s.ues[ueId].ongoingTask = false case <-sigStop: @@ -207,6 +209,7 @@ func (e *ueTaskExecuter) Run() { } e.running = true + log.Infof("simulation started for ue %d", e.UeId) e.timedTasks = map[Task]*time.Timer{} e.state = ueCtx.MM5G_NULL e.targetState = ueCtx.MM5G_NULL @@ -215,156 +218,171 @@ func (e *ueTaskExecuter) Run() { } func (e *ueTaskExecuter) listen() { - log.Infof("simulation started for ue %d", e.UeId) loop := true for loop { select { case task := <-e.TaskChan: - log.Debugf("------------------------------------------- received task: %s ------------------------------------------- ", task.TaskType.ToStr()) - if e.attachedGnb == "" && task.TaskType != AttachToGNB { - msg := fmt.Sprintf("UE %d is not attached to a GNB", e.UeId) - log.Error(msg) - e.ReportChan <- report{success: false, reason: msg} - } - if task.Delay > 0 { - since := time.Since(e.lastReceivedTaskTime).Milliseconds() - log.Info(since) - task.Delay = max(0, task.Delay-int(time.Since(e.lastReceivedTaskTime).Milliseconds())) - } - e.lastReceivedTaskTime = time.Now() - switch task.TaskType { - case AttachToGNB: - if e.Gnbs[task.Parameters.GnbId] != nil { - // Create a new UE coroutine - // ue.NewUE returns context of the new UE - e.lock.Lock() - e.ueRx = make(chan procedures.UeTesterMessage) - e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.Wg) - e.Wg.Add(1) - e.attachedGnb = task.Parameters.GnbId - e.lock.Unlock() - e.ReportChan <- report{success: true} - } else { - msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) - log.Error(msg) - e.ReportChan <- report{success: false, reason: msg} - } - case Registration: - if e.toBeKilled != nil { - e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Kill} - } - e.sendProcedureToUE(task, procedures.Registration) - e.targetState = ueCtx.MM5G_REGISTERED - case Deregistration: - e.sendProcedureToUE(task, procedures.Deregistration) - e.targetState = ueCtx.MM5G_DEREGISTERED - case Idle: - e.sendProcedureToUE(task, procedures.Idle) - e.targetState = ueCtx.MM5G_IDLE - case NewPDUSession: - if e.state == ueCtx.MM5G_REGISTERED { - e.sendProcedureToUE(task, procedures.NewPDUSession) - if !task.Hang { - // TODO: Implement way to check if pduSession is successful - time.Sleep(2 * time.Second) - e.ReportChan <- report{success: true, reason: "sent PDU session request"} - } - } else { - msg := fmt.Sprintf("UE %d is not registered", e.UeId) - log.Error(msg) - e.ReportChan <- report{success: false, reason: msg} - } - case XNHandover: - if e.Gnbs[task.Parameters.GnbId] != nil { - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - trigger.TriggerXnHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) - e.attachedGnb = task.Parameters.GnbId - // Wait for succesful handover - // TODO: We should wait for confimation from gnb instead of timer - time.Sleep(2 * time.Second) - e.ReportChan <- report{success: true, reason: "succesful XNHandover"} - }) - e.targetState = ueCtx.MM5G_REGISTERED - } else { - msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) - log.Error(msg) - e.ReportChan <- report{success: false, reason: msg} - } - case NGAPHandover: - if e.Gnbs[task.Parameters.GnbId] != nil { - time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - trigger.TriggerNgapHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) - e.attachedGnb = task.Parameters.GnbId - // Wait for succesful handover - // TODO: We should wait for confimation from gnb instead of timer - time.Sleep(2 * time.Second) - e.ReportChan <- report{success: true, reason: "succesful NGAPHandover"} - }) - e.targetState = ueCtx.MM5G_REGISTERED - } else { - msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) - log.Error(msg) - e.ReportChan <- report{success: false, reason: msg} - } - case Terminate: - e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - e.lock.Lock() - defer e.lock.Unlock() - if e.toBeKilled != nil { - e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Terminate} - } - e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} - e.ueRx = nil - e.ueTx = nil - e.state = ueCtx.MM5G_NULL - e.attachedGnb = "" - for t := range e.timedTasks { - e.timedTasks[t].Stop() - delete(e.timedTasks, t) - } - e.ReportChan <- report{success: true, reason: "UE Terminated"} - }) - case Kill: - e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - e.lock.Lock() - defer e.lock.Unlock() - e.toBeKilled = e.ueRx - e.ueRx = nil - e.ueTx = nil - e.state = ueCtx.MM5G_NULL - e.attachedGnb = "" - for t := range e.timedTasks { - e.timedTasks[t].Stop() - delete(e.timedTasks, t) - } - e.ReportChan <- report{success: true, reason: "UE Killed"} - }) - } - + e.handleTaskTimer(task) case ueMsg, open := <-e.ueTx: if open { - msg := fmt.Sprintf("[UE-%d] Switched from state %d to state %d ", e.UeId, e.state, ueMsg.StateChange) - log.Info(msg) - if ueMsg.StateChange == e.targetState { - e.ReportChan <- report{success: true, reason: msg} - } - e.state = ueMsg.StateChange + e.handleUeMsg(ueMsg) } } } e.running = false } -func (e *ueTaskExecuter) sendProcedureToUE(task Task, procedure procedures.UeTesterMessageType) { +func (e *ueTaskExecuter) handleUeMsg(ueMsg scenario.ScenarioMessage) { + msg := fmt.Sprintf("[UE-%d] Switched from state %d to state %d ", e.UeId, e.state, ueMsg.StateChange) + if ueMsg.StateChange == e.targetState { + e.ReportChan <- report{success: true, reason: msg} + } + e.state = ueMsg.StateChange +} + +func (e *ueTaskExecuter) handleTaskTimer(task Task) { if task.Delay > 0 { + task.Delay = max(0, task.Delay-int(time.Since(e.lastReceivedTaskTime).Milliseconds())) e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - e.ueRx <- procedures.UeTesterMessage{Type: procedure} + e.handleTask(task) e.lock.Lock() - e.timedTasks[task].Stop() delete(e.timedTasks, task) e.lock.Unlock() }) } else { - e.ueRx <- procedures.UeTesterMessage{Type: procedure} + e.handleTask(task) + } + e.lastReceivedTaskTime = time.Now() +} + +func (e *ueTaskExecuter) handleTask(task Task) { + if log.GetLevel() >= 5 { + log.Debugf("------------------------------------ Task UE-%d ------------------------------------", e.UeId) + log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) + log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) + for i := range e.Gnbs { + for j := 1; j <= 3; j++ { + _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(j)) + if err == nil { + log.Debugf("GNB %s has ue %d context", e.Gnbs[i].GetGnbId(), j) + } + } + } + } + + if e.attachedGnb == "" && task.TaskType != AttachToGNB { + msg := fmt.Sprintf("UE %d is not attached to a GNB", e.UeId) + e.ReportChan <- report{success: false, reason: msg} + } + + switch task.TaskType { + case AttachToGNB: + if e.Gnbs[task.Parameters.GnbId] != nil { + // Create a new UE coroutine + // ue.NewUE returns context of the new UE + e.lock.Lock() + e.ueRx = make(chan procedures.UeTesterMessage) + e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.Wg) + e.attachedGnb = task.Parameters.GnbId + e.lock.Unlock() + e.ReportChan <- report{success: true} + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + e.ReportChan <- report{success: false, reason: msg} + } + case Registration: + if e.toBeKilled != nil { + e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Kill} + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} + e.targetState = ueCtx.MM5G_REGISTERED + case Deregistration: + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Deregistration} + e.targetState = ueCtx.MM5G_DEREGISTERED + case Idle: + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Idle} + e.targetState = ueCtx.MM5G_IDLE + // TODO Should sync with gnb before sending Idle success report + case ServiceRequest: + e.ueRx <- procedures.UeTesterMessage{Type: procedures.ServiceRequest} + e.targetState = ueCtx.MM5G_REGISTERED + case NewPDUSession: + if e.state == ueCtx.MM5G_REGISTERED { + log.Debugf("UE %d Before PDU session creation", e.UeId) + for i := range e.Gnbs { + _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(e.UeId)) + if err == nil { + log.Debugf("GNB %s has ue %d", e.Gnbs[i].GetGnbId(), e.UeId) + } else { + log.Debugf("GNB %s does not have %d: %s", e.Gnbs[i].GetGnbId(), e.UeId, err) + } + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} + if !task.Hang { + // TODO: Implement way to check if pduSession is successful + time.Sleep(2 * time.Second) + e.ReportChan <- report{success: true, reason: "sent PDU session request"} + } + } else { + msg := fmt.Sprintf("UE %d is not registered", e.UeId) + e.ReportChan <- report{success: false, reason: msg} + } + case XNHandover: + if e.Gnbs[task.Parameters.GnbId] != nil { + trigger.TriggerXnHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) + e.attachedGnb = task.Parameters.GnbId + // Wait for succesful handover + // TODO: We should wait for confimation from gnb instead of timer + time.Sleep(2 * time.Second) + e.ReportChan <- report{success: true, reason: "succesful XNHandover"} + e.targetState = ueCtx.MM5G_REGISTERED + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + e.ReportChan <- report{success: false, reason: msg} + } + case NGAPHandover: + if e.Gnbs[task.Parameters.GnbId] != nil { + trigger.TriggerNgapHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) + e.attachedGnb = task.Parameters.GnbId + // Wait for succesful handover + // TODO: We should wait for confimation from gnb instead of timer + time.Sleep(2 * time.Second) + e.ReportChan <- report{success: true, reason: "succesful NGAPHandover"} + e.targetState = ueCtx.MM5G_REGISTERED + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + e.ReportChan <- report{success: false, reason: msg} + } + case Terminate: + + if e.toBeKilled != nil { + e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Terminate} + e.lock.Lock() + e.toBeKilled = nil + e.lock.Unlock() + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} + e.reset() + e.ReportChan <- report{success: true, reason: "UE Terminated"} + case Kill: + // Not killing immediately to be able to deregister ue when receiving terminate command right after killing command + e.lock.Lock() + e.toBeKilled = e.ueRx + e.lock.Unlock() + e.reset() + e.ReportChan <- report{success: true, reason: "UE Killed"} + } +} + +func (e *ueTaskExecuter) reset() { + e.lock.Lock() + e.ueRx = nil + e.ueTx = nil + e.state = ueCtx.MM5G_NULL + e.attachedGnb = "" + for t := range e.timedTasks { + e.timedTasks[t].Stop() + delete(e.timedTasks, t) } + e.lock.Unlock() } diff --git a/internal/scenario/task.go b/internal/scenario/task.go index 84b55f43..1c3502ca 100644 --- a/internal/scenario/task.go +++ b/internal/scenario/task.go @@ -24,6 +24,7 @@ const ( Terminate Kill Idle + ServiceRequest NGAPHandover XNHandover ) @@ -46,6 +47,8 @@ func (t TaskType) ToStr() string { return "Kill" case Idle: return "Idle" + case ServiceRequest: + return "ServiceRequest" case NGAPHandover: return "NGAPHandover" case XNHandover: diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index 2af45695..1e28f970 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -8,215 +8,141 @@ import ( "fmt" "my5G-RANTester/config" "my5G-RANTester/internal/common/tools" - "my5G-RANTester/internal/control_test_engine/procedures" "my5G-RANTester/internal/scenario" - "os" - "os/signal" "sort" "strconv" - "sync" - "time" log "github.com/sirupsen/logrus" ) -func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb bool, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, timeBeforeReconnecting int, numPduSessions int) { - if tunnelMode != config.TunnelDisabled { - if !dedicatedGnb { - log.Fatal("You cannot use the --tunnel option, without using the --dedicatedGnb option") - } - if timeBetweenRegistration < 500 { - log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") - } +func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb bool, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, timeBeforeServiceRequest int, numPduSessions int) { + if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { + log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") } if numPduSessions > 16 { log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") } - wg := sync.WaitGroup{} - cfg := config.GetConfig() - - var numGnb int - if dedicatedGnb { - numGnb = numUes - } else { - numGnb = 1 - } - if numGnb <= 1 && (timeBeforeXnHandover != 0 || timeBeforeNgapHandover != 0) { - log.Warn("[TESTER] We are increasing the number of gNodeB to two for handover test cases. Make you sure you fill the requirements for having two gNodeBs.") - numGnb++ - } - gnbs := tools.CreateGnbs(numGnb, cfg, &wg) - - // Wait for gNB to be connected before registering UEs - // TODO: We should wait for NGSetupResponse instead - time.Sleep(1 * time.Second) - cfg.Ue.TunnelMode = tunnelMode - scenarioChans := make([]chan procedures.UeTesterMessage, numUes+1) + gnb := cfg.GNodeB + gnbs := []config.GNodeB{gnb} + nextgnb := 0 + if timeBeforeNgapHandover > 0 || timeBeforeXnHandover > 0 { + gnb = nextGnbConf(gnb, 1, cfg.GNodeB.PlmnList.GnbId) + gnbs = append(gnbs, gnb) + } + ueScenarios := []scenario.UEScenario{} + for i := 0; i < numUes; i++ { + if dedicatedGnb && i != 0 { + gnb = nextGnbConf(gnb, len(gnbs), cfg.GNodeB.PlmnList.GnbId) + gnbs = append(gnbs, gnb) + nextgnb = len(gnbs) - 1 + if timeBeforeNgapHandover > 0 || timeBeforeXnHandover > 0 { + gnb = nextGnbConf(gnb, len(gnbs), cfg.GNodeB.PlmnList.GnbId) + gnbs = append(gnbs, gnb) + } + } - sigStop := make(chan os.Signal, 1) - signal.Notify(sigStop, os.Interrupt) + tasks := []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{gnbs[nextgnb].PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + } - ueSimCfg := tools.UESimulationConfig{ - Gnbs: gnbs, - Cfg: cfg, - TimeBeforeDeregistration: timeBeforeDeregistration, - TimeBeforeNgapHandover: timeBeforeNgapHandover, - TimeBeforeXnHandover: timeBeforeXnHandover, - TimeBeforeIdle: timeBeforeIdle, - TimeBeforeReconnecting: timeBeforeReconnecting, - NumPduSessions: numPduSessions, - } + for i := 0; i < numPduSessions; i++ { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NewPDUSession, + }) + } + + if timeBeforeNgapHandover != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.NGAPHandover, + Delay: timeBeforeNgapHandover, + }) + } - stopSignal := true - for stopSignal { - // If CTRL-C signal has been received, - // stop creating new UEs, else we create numUes UEs - for ueSimCfg.UeId = 1; stopSignal && ueSimCfg.UeId <= numUes; ueSimCfg.UeId++ { - // If there is currently a coroutine handling current UE - // kill it, before creating a new coroutine with same UE - // Use case: Registration of N UEs in loop, when loop = true - if scenarioChans[ueSimCfg.UeId] != nil { - scenarioChans[ueSimCfg.UeId] <- procedures.UeTesterMessage{Type: procedures.Kill} - close(scenarioChans[ueSimCfg.UeId]) - scenarioChans[ueSimCfg.UeId] = nil + if timeBeforeXnHandover != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.XNHandover, + Delay: timeBeforeXnHandover, + }) + } + if timeBeforeIdle != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Idle, + Delay: timeBeforeIdle, + }) + if timeBeforeServiceRequest != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.ServiceRequest, + Delay: timeBeforeIdle + timeBeforeServiceRequest, + }) } - scenarioChans[ueSimCfg.UeId] = make(chan procedures.UeTesterMessage) - ueSimCfg.ScenarioChan = scenarioChans[ueSimCfg.UeId] + } - tools.SimulateSingleUE(ueSimCfg, &wg) + if timeBeforeDeregistration != 0 { + tasks = append(tasks, scenario.Task{ + TaskType: scenario.Deregistration, + Delay: timeBeforeDeregistration, + }) + } + + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].Delay < tasks[j].Delay + }) - // Before creating a new UE, we wait for timeBetweenRegistration ms - time.Sleep(time.Duration(timeBetweenRegistration) * time.Millisecond) + sumDelay := 0 + for j := 2; j < len(tasks); j++ { + tasks[j].Delay = max(0, tasks[j].Delay-sumDelay) + sumDelay += tasks[j].Delay - select { - case <-sigStop: - stopSignal = false - default: + if tasks[j].TaskType == scenario.NGAPHandover || tasks[j].TaskType == scenario.XNHandover { + tasks[j].Parameters.GnbId = gnbs[(len(gnbs)-2)+(nextgnb+1)%2].PlmnList.GnbId } } - // If loop = false, we don't go over the for loop a second time - // and we only do the numUes registration once - if !loop { - break - } - } - if stopSignal { - <-sigStop - } - for _, scenarioChan := range scenarioChans { - if scenarioChan != nil { - scenarioChan <- procedures.UeTesterMessage{Type: procedures.Terminate} + tasks[1].Delay = i * timeBetweenRegistration + + ueCfg := cfg.Ue + ueCfg.Msin = tools.IncrementMsin(i+1, cfg.Ue.Msin) + ueScenario := scenario.UEScenario{ + Config: ueCfg, + Tasks: tasks, } - } - time.Sleep(time.Second * 1) -} + if loop { + ueScenario.Loop = timeBetweenRegistration // TODO: change this! + } -func TestsingleUePdu(tunnelMode config.TunnelMode, loop bool, timeBetweenRegistration int, timeBeforeDeregistration int, timeBeforeNgapHandover int, timeBeforeXnHandover int, timeBeforeIdle int, numPduSessions int) { - if tunnelMode != config.TunnelDisabled && timeBetweenRegistration < 500 { - log.Fatal("When using the --tunnel option, --timeBetweenRegistration must be equal to at least 500 ms, or else gtp5g kernel module may crash if you create tunnels too rapidly.") + ueScenarios = append(ueScenarios, ueScenario) } - if numPduSessions > 16 { - log.Fatal("You can't have more than 16 PDU Sessions per UE as per spec.") - } - - cfg := config.GetConfig() + r := scenario.ScenarioManager{} + r.Start(gnbs, cfg.AMF, ueScenarios) +} +func nextGnbConf(gnb config.GNodeB, i int, baseId string) config.GNodeB { var err error - gnb2 := cfg.GNodeB - gnb2.PlmnList.GnbId = genereateGnbId(1, cfg.GNodeB.PlmnList.GnbId) - - gnb2.ControlIF.Ip, err = tools.IncrementIP(cfg.GNodeB.ControlIF.Ip, "0.0.0.0/0") + gnb.PlmnList.GnbId = genereateGnbId(i, baseId) + gnb.ControlIF.Ip, err = tools.IncrementIP(gnb.ControlIF.Ip, "0.0.0.0/0") if err != nil { log.Fatal("[GNB][CONFIG] Error while allocating ip for N2: " + err.Error()) } - gnb2.DataIF.Ip, err = tools.IncrementIP(cfg.GNodeB.DataIF.Ip, "0.0.0.0/0") + gnb.DataIF.Ip, err = tools.IncrementIP(gnb.DataIF.Ip, "0.0.0.0/0") if err != nil { log.Fatal("[GNB][CONFIG] Error while allocating ip for N3: " + err.Error()) } - - gnbs := []config.GNodeB{cfg.GNodeB, gnb2} - nextgnb := 0 - - tasks := []scenario.Task{ - { - TaskType: scenario.AttachToGNB, - Parameters: struct { - GnbId string - }{gnbs[nextgnb].PlmnList.GnbId}, - }, - { - TaskType: scenario.Registration, - }, - } - - for i := 0; i < numPduSessions; i++ { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.NewPDUSession, - }) - } - - if timeBeforeNgapHandover != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.NGAPHandover, - Delay: timeBeforeNgapHandover, - }) - } - - if timeBeforeXnHandover != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.XNHandover, - Delay: timeBeforeXnHandover, - }) - } - if timeBeforeIdle != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.Idle, - Delay: timeBeforeIdle, - }) - } - - if timeBeforeDeregistration != 0 { - tasks = append(tasks, scenario.Task{ - TaskType: scenario.Deregistration, - Delay: timeBeforeDeregistration, - }) - } - - sort.Slice(tasks, func(i, j int) bool { - return tasks[i].Delay < tasks[j].Delay - }) - - sumDelay := 0 - for i := 0; i < len(tasks); i++ { - tasks[i].Delay = tasks[i].Delay - sumDelay - sumDelay += tasks[i].Delay - - if tasks[i].TaskType == scenario.NGAPHandover || tasks[i].TaskType == scenario.XNHandover { - nextgnb = (nextgnb + 1) % 2 - tasks[i].Parameters.GnbId = gnbs[nextgnb].PlmnList.GnbId - } - } - - ueScenario := scenario.UEScenario{ - Config: cfg.Ue, - Tasks: tasks, - } - ueScenarios := []scenario.UEScenario{ueScenario} - - if loop { - ueScenario.Loop = timeBetweenRegistration - } - - r := scenario.ScenarioManager{} - r.Start(gnbs, cfg.AMF, ueScenarios, 0) + return gnb } func genereateGnbId(i int, gnbId string) string { diff --git a/test/pr_test.go b/test/pr_test.go index 679b8e1e..0d983260 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -99,7 +99,7 @@ func TestSingleUe(t *testing.T) { ueScenarios := []scenario.UEScenario{ueScenario} r := scenario.ScenarioManager{} - r.Start(gnbs, conf.AMF, ueScenarios, 0) + r.Start(gnbs, conf.AMF, ueScenarios) time.Sleep(time.Duration(1000) * time.Millisecond) From 80f2d7f9ebfe121504ce1b49509fdca401770dd1 Mon Sep 17 00:00:00 2001 From: Raguideau Date: Tue, 26 Mar 2024 15:10:04 +0000 Subject: [PATCH 5/9] fix cpu consumption & separate taskExecuter into functions Signed-off-by: Raguideau --- internal/control_test_engine/ue/ue.go | 2 +- internal/scenario/scenario.go | 377 +----------------- internal/scenario/scenarioManager.go | 199 +++++++++ internal/scenario/task.go | 2 +- internal/scenario/ueTaskExecuter.go | 300 ++++++++++++++ internal/templates/test-multi-ues-in-queue.go | 10 +- test/pr_test.go | 4 +- 7 files changed, 510 insertions(+), 384 deletions(-) create mode 100644 internal/scenario/scenarioManager.go create mode 100644 internal/scenario/ueTaskExecuter.go diff --git a/internal/control_test_engine/ue/ue.go b/internal/control_test_engine/ue/ue.go index 8af11771..77c96f72 100644 --- a/internal/control_test_engine/ue/ue.go +++ b/internal/control_test_engine/ue/ue.go @@ -59,7 +59,7 @@ func NewUE(conf config.Ue, id int, ueMgrChannel chan procedures.UeTesterMessage, select { case msg, open := <-ue.GetGnbTx(): if !open { - log.Warn("[UE][", ue.GetMsin(), "] Stopping UE as communication with gNB was closed") + log.Warn("[UE][", ue.GetMsin(), "] communication with gNB was closed") ue.SetGnbTx(nil) break } diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index 988ac7f2..b8b0e202 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -1,388 +1,17 @@ /** * SPDX-License-Identifier: Apache-2.0 - * © Copyright 2023 Hewlett Packard Enterprise Development LP + * © Copyright 2024 Hewlett Packard Enterprise Development LP */ package scenario import ( - "fmt" "my5G-RANTester/config" - "my5G-RANTester/internal/control_test_engine/gnb" - "my5G-RANTester/internal/control_test_engine/gnb/context" - "my5G-RANTester/internal/control_test_engine/gnb/ngap/trigger" - "my5G-RANTester/internal/control_test_engine/procedures" - "my5G-RANTester/internal/control_test_engine/ue" - ueCtx "my5G-RANTester/internal/control_test_engine/ue/context" - "my5G-RANTester/internal/control_test_engine/ue/scenario" - "os" - "os/signal" - "sync" - "time" - - log "github.com/sirupsen/logrus" ) -type ScenarioManager struct { - gnbs map[string]*context.GNBContext - ues map[int]*ueScenarioCtx -} - // Description of the scenario to be run for one UE type UEScenario struct { Config config.Ue Tasks []Task - Loop int // Time between two loops - ForceStop int // Time before forcefully stoping scenario (0 to desactivate) -} - -// Context of running ue scenario -type ueScenarioCtx struct { - ongoingTask bool - nextTasks []Task - taskChan chan Task - reportChan chan report - loopTicker *time.Ticker - endTicker *time.Ticker -} - -type report struct { - success bool - reason string -} - -func (s *ScenarioManager) Start(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario) { - - wg := sync.WaitGroup{} - - s.gnbs = make(map[string]*context.GNBContext) - - for gnbConf := range gnbConfs { - s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = gnb.InitGnb2(gnbConfs[gnbConf], amfConf, &wg) // TODO: replace InitGNB2 with InitGNB - wg.Add(1) - } - - // Wait for gNB to be connected before registering UEs - // TODO: We should wait for NGSetupResponse instead - time.Sleep(2 * time.Second) - - s.ues = make(map[int]*ueScenarioCtx) - - sigStop := make(chan os.Signal, 1) - signal.Notify(sigStop, os.Interrupt) - stopSignal := false - for !stopSignal { - done := true - for ueId := 1; !stopSignal && ueId <= len(ueScenarios); ueId++ { - if s.ues[ueId] == nil { - ue := ueScenarioCtx{ - ongoingTask: false, - nextTasks: ueScenarios[ueId-1].Tasks, - taskChan: make(chan Task, 1), - reportChan: make(chan report, 1), - } - - if ueScenarios[ueId-1].Loop > 0 { - ue.loopTicker = time.NewTicker(time.Duration(ueScenarios[ueId-1].Loop) * time.Millisecond) - } - if ueScenarios[ueId-1].ForceStop > 0 { - ue.endTicker = time.NewTicker(time.Duration(ueScenarios[ueId-1].ForceStop) * time.Millisecond) - } - - executer := ueTaskExecuter{ - UeId: ueId, - UeCfg: ueScenarios[ueId-1].Config, - TaskChan: ue.taskChan, - ReportChan: ue.reportChan, - Gnbs: s.gnbs, - Wg: &wg, - } - executer.Run() - - s.ues[ueId] = &ue - } - - select { - case report := <-s.ues[ueId].reportChan: - log.Debugf("------------------------------------ Report UE-%d ------------------------------------", ueId) - if !report.success { - s.ues[ueId].nextTasks = []Task{} - log.Errorf("[UE-%d] Reported error: %s", ueId, report.reason) - } else { - log.Infof("[UE-%d] Reported succes: %s", ueId, report.reason) - } - s.ues[ueId].ongoingTask = false - case <-sigStop: - stopSignal = true - default: - } - if ueScenarios[ueId-1].ForceStop > 0 { - select { - case <-s.ues[ueId].endTicker.C: - done = true - log.Debugf("[UE-%d]Force end of scenario", ueId) - continue - default: - } - } - if ueScenarios[ueId-1].Loop > 0 { - done = false - select { - case <-s.ues[ueId].loopTicker.C: - log.Debugf("[UE-%d] starting new loop", ueId) - s.ues[ueId].taskChan <- Task{TaskType: Kill} - s.ues[ueId].nextTasks = ueScenarios[ueId-1].Tasks - s.ues[ueId].ongoingTask = true - continue - default: - } - } - - if s.ues[ueId].ongoingTask { - done = false - } else { - if len(s.ues[ueId].nextTasks) > 0 { - s.ues[ueId].taskChan <- s.ues[ueId].nextTasks[0] - s.ues[ueId].nextTasks = s.ues[ueId].nextTasks[1:] - s.ues[ueId].ongoingTask = true - done = false - } - } - - } - if done { - stopSignal = true - } - } - for _, ue := range s.ues { - ue.taskChan <- Task{ - TaskType: Terminate, - } - } - - time.Sleep(time.Second * 3) -} - -type ueTaskExecuter struct { - UeId int - UeCfg config.Ue - TaskChan chan Task - ReportChan chan report - Gnbs map[string]*context.GNBContext - Wg *sync.WaitGroup - - running bool - ueRx chan procedures.UeTesterMessage - ueTx chan scenario.ScenarioMessage - attachedGnb string - timedTasks map[Task]*time.Timer - lastReceivedTaskTime time.Time - state int - targetState int - lock sync.Mutex - toBeKilled chan procedures.UeTesterMessage -} - -func (e *ueTaskExecuter) Run() { - if e.running { - log.Errorf("simulation for ue %d failed: already running", e.UeId) - return - } - if e.UeCfg == (config.Ue{}) { - log.Errorf("simulation for ue %d failed: no config", e.UeId) - return - } - if e.TaskChan == nil { - log.Errorf("simulation for ue %d failed: no taskChan", e.UeId) - return - } - if e.ReportChan == nil { - log.Errorf("simulation for ue %d failed: no reportChan", e.UeId) - return - } - if e.Gnbs == nil { - log.Errorf("simulation for ue %d failed: no gnbs", e.UeId) - return - } - if e.Wg == nil { - log.Errorf("simulation for ue %d failed: no WaitGroup is given", e.UeId) - return - } - - e.running = true - log.Infof("simulation started for ue %d", e.UeId) - e.timedTasks = map[Task]*time.Timer{} - e.state = ueCtx.MM5G_NULL - e.targetState = ueCtx.MM5G_NULL - e.lastReceivedTaskTime = time.Now() - go e.listen() -} - -func (e *ueTaskExecuter) listen() { - loop := true - for loop { - select { - case task := <-e.TaskChan: - e.handleTaskTimer(task) - case ueMsg, open := <-e.ueTx: - if open { - e.handleUeMsg(ueMsg) - } - } - } - e.running = false -} - -func (e *ueTaskExecuter) handleUeMsg(ueMsg scenario.ScenarioMessage) { - msg := fmt.Sprintf("[UE-%d] Switched from state %d to state %d ", e.UeId, e.state, ueMsg.StateChange) - if ueMsg.StateChange == e.targetState { - e.ReportChan <- report{success: true, reason: msg} - } - e.state = ueMsg.StateChange -} - -func (e *ueTaskExecuter) handleTaskTimer(task Task) { - if task.Delay > 0 { - task.Delay = max(0, task.Delay-int(time.Since(e.lastReceivedTaskTime).Milliseconds())) - e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { - e.handleTask(task) - e.lock.Lock() - delete(e.timedTasks, task) - e.lock.Unlock() - }) - } else { - e.handleTask(task) - } - e.lastReceivedTaskTime = time.Now() -} - -func (e *ueTaskExecuter) handleTask(task Task) { - if log.GetLevel() >= 5 { - log.Debugf("------------------------------------ Task UE-%d ------------------------------------", e.UeId) - log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) - log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) - for i := range e.Gnbs { - for j := 1; j <= 3; j++ { - _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(j)) - if err == nil { - log.Debugf("GNB %s has ue %d context", e.Gnbs[i].GetGnbId(), j) - } - } - } - } - - if e.attachedGnb == "" && task.TaskType != AttachToGNB { - msg := fmt.Sprintf("UE %d is not attached to a GNB", e.UeId) - e.ReportChan <- report{success: false, reason: msg} - } - - switch task.TaskType { - case AttachToGNB: - if e.Gnbs[task.Parameters.GnbId] != nil { - // Create a new UE coroutine - // ue.NewUE returns context of the new UE - e.lock.Lock() - e.ueRx = make(chan procedures.UeTesterMessage) - e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.Wg) - e.attachedGnb = task.Parameters.GnbId - e.lock.Unlock() - e.ReportChan <- report{success: true} - } else { - msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) - e.ReportChan <- report{success: false, reason: msg} - } - case Registration: - if e.toBeKilled != nil { - e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Kill} - } - e.ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} - e.targetState = ueCtx.MM5G_REGISTERED - case Deregistration: - e.ueRx <- procedures.UeTesterMessage{Type: procedures.Deregistration} - e.targetState = ueCtx.MM5G_DEREGISTERED - case Idle: - e.ueRx <- procedures.UeTesterMessage{Type: procedures.Idle} - e.targetState = ueCtx.MM5G_IDLE - // TODO Should sync with gnb before sending Idle success report - case ServiceRequest: - e.ueRx <- procedures.UeTesterMessage{Type: procedures.ServiceRequest} - e.targetState = ueCtx.MM5G_REGISTERED - case NewPDUSession: - if e.state == ueCtx.MM5G_REGISTERED { - log.Debugf("UE %d Before PDU session creation", e.UeId) - for i := range e.Gnbs { - _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(e.UeId)) - if err == nil { - log.Debugf("GNB %s has ue %d", e.Gnbs[i].GetGnbId(), e.UeId) - } else { - log.Debugf("GNB %s does not have %d: %s", e.Gnbs[i].GetGnbId(), e.UeId, err) - } - } - e.ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} - if !task.Hang { - // TODO: Implement way to check if pduSession is successful - time.Sleep(2 * time.Second) - e.ReportChan <- report{success: true, reason: "sent PDU session request"} - } - } else { - msg := fmt.Sprintf("UE %d is not registered", e.UeId) - e.ReportChan <- report{success: false, reason: msg} - } - case XNHandover: - if e.Gnbs[task.Parameters.GnbId] != nil { - trigger.TriggerXnHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) - e.attachedGnb = task.Parameters.GnbId - // Wait for succesful handover - // TODO: We should wait for confimation from gnb instead of timer - time.Sleep(2 * time.Second) - e.ReportChan <- report{success: true, reason: "succesful XNHandover"} - e.targetState = ueCtx.MM5G_REGISTERED - } else { - msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) - e.ReportChan <- report{success: false, reason: msg} - } - case NGAPHandover: - if e.Gnbs[task.Parameters.GnbId] != nil { - trigger.TriggerNgapHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) - e.attachedGnb = task.Parameters.GnbId - // Wait for succesful handover - // TODO: We should wait for confimation from gnb instead of timer - time.Sleep(2 * time.Second) - e.ReportChan <- report{success: true, reason: "succesful NGAPHandover"} - e.targetState = ueCtx.MM5G_REGISTERED - } else { - msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) - e.ReportChan <- report{success: false, reason: msg} - } - case Terminate: - - if e.toBeKilled != nil { - e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Terminate} - e.lock.Lock() - e.toBeKilled = nil - e.lock.Unlock() - } - e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} - e.reset() - e.ReportChan <- report{success: true, reason: "UE Terminated"} - case Kill: - // Not killing immediately to be able to deregister ue when receiving terminate command right after killing command - e.lock.Lock() - e.toBeKilled = e.ueRx - e.lock.Unlock() - e.reset() - e.ReportChan <- report{success: true, reason: "UE Killed"} - } -} - -func (e *ueTaskExecuter) reset() { - e.lock.Lock() - e.ueRx = nil - e.ueTx = nil - e.state = ueCtx.MM5G_NULL - e.attachedGnb = "" - for t := range e.timedTasks { - e.timedTasks[t].Stop() - delete(e.timedTasks, t) - } - e.lock.Unlock() + Loop bool // Restart scenario once done + ForceStop int // Time before forcefully stoping scenario (0 to desactivate) } diff --git a/internal/scenario/scenarioManager.go b/internal/scenario/scenarioManager.go new file mode 100644 index 00000000..6aa75ce5 --- /dev/null +++ b/internal/scenario/scenarioManager.go @@ -0,0 +1,199 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * © Copyright 2024 Hewlett Packard Enterprise Development LP + */ +package scenario + +import ( + "my5G-RANTester/config" + "my5G-RANTester/internal/control_test_engine/gnb" + "my5G-RANTester/internal/control_test_engine/gnb/context" + "os" + "os/signal" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +type ScenarioManager struct { + gnbs map[string]*context.GNBContext + ues map[int]*ueTasksCtx + reportChan chan report + registrationQueue chan order + wg *sync.WaitGroup // TODO: not used, to remove +} + +// Context of running ue scenario +type ueTasksCtx struct { + done bool + nextTasks []Task + workerChan chan Task +} + +type report struct { + ueId int + success bool + reason string +} + +type order struct { + task Task + workerChan chan Task +} + +func (s *ScenarioManager) StartScenario(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario, timeBetweenRegistration int) { + log.Info("------------------------------------ Starting scenario ------------------------------------") + s.wg = &sync.WaitGroup{} + + s.reportChan = make(chan report, 10) + s.registrationQueue = make(chan order, len(ueScenarios)) + stopChan := make(chan int, 1) + go startRegistrationRateController(s.registrationQueue, timeBetweenRegistration, stopChan) + + s.setupGnbs(gnbConfs, amfConf) + s.ues = make(map[int]*ueTasksCtx) + + sigStop := make(chan os.Signal, 1) + signal.Notify(sigStop, os.Interrupt) + + endSenario := false + for ueId := 1; ueId <= len(ueScenarios); ueId++ { + s.setupUeTaskExecuter(ueId, ueScenarios[ueId-1]) + + select { + case <-sigStop: + endSenario = true + ueId = len(ueScenarios) + 1 // exit loop + default: + } + } + + for !endSenario { + endSenario = true + select { + case <-sigStop: + endSenario = true + case report := <-s.reportChan: + s.handleReport(report) + s.manageNextTask(report.ueId, ueScenarios) + for i := range s.ues { + if !s.ues[i].done { + endSenario = false + } + } + } + } + + for _, ue := range s.ues { + ue.workerChan <- Task{ + TaskType: Terminate, + } + } + stopChan <- 0 + + time.Sleep(time.Second * 3) + log.Info("------------------------------------ Scenario finished ------------------------------------") +} + +func (s *ScenarioManager) handleReport(report report) { + log.Debugf("------------------------------------ Report UE-%d ------------------------------------", report.ueId) + if !report.success { + s.ues[report.ueId].nextTasks = []Task{} + log.Errorf("[UE-%d] Reported error: %s", report.ueId, report.reason) + } + log.Infof("[UE-%d] Reported succes: %s", report.ueId, report.reason) +} + +func (s *ScenarioManager) manageNextTask(ueId int, ueScenarios []UEScenario) bool { + newTask := false + if len(s.ues[ueId].nextTasks) > 0 { + s.sendNextTask(ueId) + return true + } + if ueScenarios[ueId-1].Loop { + s.restartUeScenario(ueId, ueScenarios[ueId-1]) + return true + } + s.ues[ueId].done = true + return newTask +} + +func (s *ScenarioManager) sendNextTask(ueId int) { + if s.ues[ueId].nextTasks[0].TaskType == Registration { + s.registrationQueue <- order{task: s.ues[ueId].nextTasks[0], workerChan: s.ues[ueId].workerChan} + } else { + s.ues[ueId].workerChan <- s.ues[ueId].nextTasks[0] + } + s.ues[ueId].nextTasks = s.ues[ueId].nextTasks[1:] +} + +func (s *ScenarioManager) restartUeScenario(ueId int, ueScenario UEScenario) { + log.Debugf("[UE-%d] starting new loop", ueId) + s.ues[ueId].workerChan <- Task{TaskType: Kill} + s.ues[ueId].nextTasks = ueScenario.Tasks +} + +func (s *ScenarioManager) setupGnbs(gnbConfs []config.GNodeB, amfConf config.AMF) { + s.gnbs = make(map[string]*context.GNBContext) + + for gnbConf := range gnbConfs { + s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = gnb.InitGnb2(gnbConfs[gnbConf], amfConf, s.wg) // TODO: replace InitGNB2 with InitGNB + s.wg.Add(1) + } + + // Wait for gNB to be connected before registering UEs + // TODO: We should wait for NGSetupResponse instead + time.Sleep(2 * time.Second) +} + +func (s *ScenarioManager) setupUeTaskExecuter(ueId int, ueScenario UEScenario) { + ue := ueTasksCtx{ + done: false, + nextTasks: ueScenario.Tasks, + workerChan: make(chan Task, 1), + } + + if ueScenario.ForceStop > 0 { + time.AfterFunc(time.Duration(ueScenario.ForceStop)*time.Millisecond, func() { + ue.workerChan <- Task{TaskType: Terminate} + ue.done = true + }) + } + + worker := ueTaskExecuter{ + UeId: ueId, + UeCfg: ueScenario.Config, + TaskChan: ue.workerChan, + ReportChan: s.reportChan, + Gnbs: s.gnbs, + wg: s.wg, + } + worker.Run() + + s.ues[ueId] = &ue +} + +func startRegistrationRateController(orderChan chan order, timeBetweenRegistration int, stopChan chan int) { + waitTime := time.Duration(timeBetweenRegistration) * time.Millisecond + registrationTicker := time.NewTicker(waitTime) + log.Debug("Started registration rate controller") + for { + log.Debug("starting new register ticker loop") + select { + case <-registrationTicker.C: + log.Debug("registration rate controller tick! stoping ticker") + registrationTicker.Stop() + select { + case order := <-orderChan: + log.Debug("found register order, forwading task and resetting timer") + order.workerChan <- order.task + registrationTicker.Reset(waitTime) + case <-stopChan: + return + } + case <-stopChan: + return + } + } +} diff --git a/internal/scenario/task.go b/internal/scenario/task.go index 1c3502ca..e8653f49 100644 --- a/internal/scenario/task.go +++ b/internal/scenario/task.go @@ -1,6 +1,6 @@ /** * SPDX-License-Identifier: Apache-2.0 - * © Copyright 2023 Hewlett Packard Enterprise Development LP + * © Copyright 2024 Hewlett Packard Enterprise Development LP */ package scenario diff --git a/internal/scenario/ueTaskExecuter.go b/internal/scenario/ueTaskExecuter.go new file mode 100644 index 00000000..a7bfa139 --- /dev/null +++ b/internal/scenario/ueTaskExecuter.go @@ -0,0 +1,300 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * © Copyright 2024 Hewlett Packard Enterprise Development LP + */ +package scenario + +import ( + "fmt" + "my5G-RANTester/config" + "my5G-RANTester/internal/control_test_engine/gnb/context" + "my5G-RANTester/internal/control_test_engine/gnb/ngap/trigger" + "my5G-RANTester/internal/control_test_engine/procedures" + "my5G-RANTester/internal/control_test_engine/ue" + ueCtx "my5G-RANTester/internal/control_test_engine/ue/context" + "my5G-RANTester/internal/control_test_engine/ue/scenario" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +type ueTaskExecuter struct { + UeId int + UeCfg config.Ue + TaskChan chan Task + ReportChan chan report + Gnbs map[string]*context.GNBContext + + wg *sync.WaitGroup + running bool + ueRx chan procedures.UeTesterMessage + ueTx chan scenario.ScenarioMessage + attachedGnb string + timedTasks map[Task]*time.Timer + lastReceivedTaskTime time.Time + state int + targetState int + lock sync.Mutex + toBeKilled chan procedures.UeTesterMessage + loop bool +} + +func (e *ueTaskExecuter) Run() { + if e.running { + log.Errorf("simulation for ue %d failed: already running", e.UeId) + return + } + if e.UeCfg == (config.Ue{}) { + log.Errorf("simulation for ue %d failed: no config", e.UeId) + return + } + if e.TaskChan == nil { + log.Errorf("simulation for ue %d failed: no taskChan", e.UeId) + return + } + if e.ReportChan == nil { + log.Errorf("simulation for ue %d failed: no reportChan", e.UeId) + return + } + if e.Gnbs == nil { + log.Errorf("simulation for ue %d failed: no gnbs", e.UeId) + return + } + if e.wg == nil { + log.Errorf("simulation for ue %d failed: no WaitGroup is given", e.UeId) + return + } + + e.running = true + + e.timedTasks = map[Task]*time.Timer{} + e.state = ueCtx.MM5G_NULL + e.targetState = ueCtx.MM5G_NULL + e.lastReceivedTaskTime = time.Now() + go e.listen() +} + +func (e *ueTaskExecuter) listen() { + e.loop = true + log.Infof("simulation started for ue %d", e.UeId) + e.sendReport(true, "simulation started") + for e.loop { + select { + case task := <-e.TaskChan: + e.handleTaskTimer(task) + case ueMsg, open := <-e.ueTx: + if open { + e.handleUeMsg(ueMsg) + } + } + } + e.running = false +} + +func (e *ueTaskExecuter) handleUeMsg(ueMsg scenario.ScenarioMessage) { + msg := fmt.Sprintf("Switched from state %d to state %d ", e.state, ueMsg.StateChange) + if ueMsg.StateChange == e.targetState { + e.sendReport(true, msg) + } + e.state = ueMsg.StateChange +} + +func (e *ueTaskExecuter) handleTaskTimer(task Task) { + if task.Delay > 0 { + task.Delay = max(0, task.Delay-int(time.Since(e.lastReceivedTaskTime).Milliseconds())) + e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { + e.handleTask(task) + e.lock.Lock() + delete(e.timedTasks, task) + e.lock.Unlock() + }) + } else { + e.handleTask(task) + } + e.lastReceivedTaskTime = time.Now() +} + +func (e *ueTaskExecuter) handleTask(task Task) { + if log.GetLevel() >= 5 { + log.Debugf("------------------------------------ Task UE-%d ------------------------------------", e.UeId) + log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) + log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) + for i := range e.Gnbs { + for j := 1; j <= 3; j++ { + _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(j)) + if err == nil { + log.Debugf("GNB %s has ue %d context", e.Gnbs[i].GetGnbId(), j) + } + } + } + } + + if e.attachedGnb == "" && task.TaskType != AttachToGNB { + msg := fmt.Sprintf("UE %d is not attached to a GNB", e.UeId) + e.sendReport(false, msg) + } + + e.dispatchTask(task) +} + +func (e *ueTaskExecuter) dispatchTask(task Task) { + switch task.TaskType { + case AttachToGNB: + e.handleAttachToGnb(task) + case Registration: + e.handleRegistration() + case Deregistration: + e.handleDeregistration() + case Idle: + e.handleIdle() + case ServiceRequest: + e.handleServiceRequest() + case NewPDUSession: + e.handleNewPduSession(task) + case XNHandover: + e.handleXnHandover(task) + case NGAPHandover: + e.handleNgapHandover(task) + case Terminate: + e.handleTerminate() + case Kill: + e.handleKill() + default: + e.sendReport(false, "unknown task") + } +} + +func (e *ueTaskExecuter) handleAttachToGnb(task Task) { + if e.Gnbs[task.Parameters.GnbId] != nil { + // Create a new UE coroutine + // ue.NewUE returns context of the new UE + e.lock.Lock() + e.ueRx = make(chan procedures.UeTesterMessage) + e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.wg) + e.wg.Add(1) + e.attachedGnb = task.Parameters.GnbId + e.lock.Unlock() + e.sendReport(true, "UE successfully attached to GNB "+task.Parameters.GnbId) + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + e.sendReport(false, msg) + } +} + +func (e *ueTaskExecuter) handleRegistration() { + if e.toBeKilled != nil { + e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Kill} + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} + e.targetState = ueCtx.MM5G_REGISTERED +} + +func (e *ueTaskExecuter) handleDeregistration() { + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Deregistration} + e.targetState = ueCtx.MM5G_DEREGISTERED +} + +func (e *ueTaskExecuter) handleIdle() { + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Idle} + e.targetState = ueCtx.MM5G_IDLE + // TODO Should sync with gnb before sending Idle success report +} + +func (e *ueTaskExecuter) handleServiceRequest() { + e.ueRx <- procedures.UeTesterMessage{Type: procedures.ServiceRequest} + e.targetState = ueCtx.MM5G_REGISTERED +} + +func (e *ueTaskExecuter) handleNewPduSession(task Task) { + if e.state == ueCtx.MM5G_REGISTERED { + log.Debugf("UE %d Before PDU session creation", e.UeId) + for i := range e.Gnbs { + _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(e.UeId)) + if err == nil { + log.Debugf("GNB %s has ue %d", e.Gnbs[i].GetGnbId(), e.UeId) + } else { + log.Debugf("GNB %s does not have %d: %s", e.Gnbs[i].GetGnbId(), e.UeId, err) + } + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} + if !task.Hang { + // TODO: Implement way to check if pduSession is successful + time.Sleep(2 * time.Second) + e.sendReport(true, "sent PDU session request") + } + } else { + msg := fmt.Sprintf("UE %d is not registered", e.UeId) + e.sendReport(false, msg) + } +} + +func (e *ueTaskExecuter) handleXnHandover(task Task) { + if e.Gnbs[task.Parameters.GnbId] != nil { + trigger.TriggerXnHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) + e.attachedGnb = task.Parameters.GnbId + // Wait for succesful handover + // TODO: We should wait for confimation from gnb instead of timer + time.Sleep(2 * time.Second) + e.sendReport(true, "succesful XNHandover") + e.targetState = ueCtx.MM5G_REGISTERED + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + e.sendReport(false, msg) + } +} + +func (e *ueTaskExecuter) handleNgapHandover(task Task) { + if e.Gnbs[task.Parameters.GnbId] != nil { + trigger.TriggerNgapHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) + e.attachedGnb = task.Parameters.GnbId + // Wait for succesful handover + // TODO: We should wait for confimation from gnb instead of timer + time.Sleep(2 * time.Second) + e.sendReport(true, "succesful NGAPHandover") + e.targetState = ueCtx.MM5G_REGISTERED + } else { + msg := fmt.Sprintf("GNB %s not found", task.Parameters.GnbId) + e.sendReport(false, msg) + } +} + +func (e *ueTaskExecuter) handleTerminate() { + if e.toBeKilled != nil { + e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Terminate} + e.lock.Lock() + e.toBeKilled = nil + e.lock.Unlock() + } + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} + e.reset() + e.sendReport(true, "UE Terminated") + close(e.TaskChan) + e.loop = false +} + +func (e *ueTaskExecuter) handleKill() { + // Not killing immediately to be able to deregister ue when receiving terminate command right after killing command + e.lock.Lock() + e.toBeKilled = e.ueRx + e.lock.Unlock() + e.reset() + e.sendReport(true, "UE Killed") +} + +func (e *ueTaskExecuter) reset() { + e.lock.Lock() + e.ueRx = nil + e.ueTx = nil + e.state = ueCtx.MM5G_NULL + e.attachedGnb = "" + for t := range e.timedTasks { + e.timedTasks[t].Stop() + delete(e.timedTasks, t) + } + e.lock.Unlock() +} + +func (e *ueTaskExecuter) sendReport(success bool, msg string) { + e.ReportChan <- report{success: success, reason: msg, ueId: e.UeId} +} diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index 1e28f970..020cb276 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -102,8 +102,8 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb }) sumDelay := 0 - for j := 2; j < len(tasks); j++ { - tasks[j].Delay = max(0, tasks[j].Delay-sumDelay) + for j := 0; j < len(tasks); j++ { + tasks[j].Delay = tasks[j].Delay - sumDelay sumDelay += tasks[j].Delay if tasks[j].TaskType == scenario.NGAPHandover || tasks[j].TaskType == scenario.XNHandover { @@ -111,8 +111,6 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb } } - tasks[1].Delay = i * timeBetweenRegistration - ueCfg := cfg.Ue ueCfg.Msin = tools.IncrementMsin(i+1, cfg.Ue.Msin) ueScenario := scenario.UEScenario{ @@ -121,14 +119,14 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb } if loop { - ueScenario.Loop = timeBetweenRegistration // TODO: change this! + ueScenario.Loop = loop } ueScenarios = append(ueScenarios, ueScenario) } r := scenario.ScenarioManager{} - r.Start(gnbs, cfg.AMF, ueScenarios) + r.StartScenario(gnbs, cfg.AMF, ueScenarios, timeBetweenRegistration) } func nextGnbConf(gnb config.GNodeB, i int, baseId string) config.GNodeB { diff --git a/test/pr_test.go b/test/pr_test.go index 0d983260..53b6f7d0 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -75,7 +75,7 @@ func TestSingleUe(t *testing.T) { gnbs := []config.GNodeB{conf.GNodeB} ueScenario := scenario.UEScenario{ Config: conf.Ue, - Loop: 500, + Loop: false, ForceStop: 20000, Tasks: []scenario.Task{ { @@ -99,7 +99,7 @@ func TestSingleUe(t *testing.T) { ueScenarios := []scenario.UEScenario{ueScenario} r := scenario.ScenarioManager{} - r.Start(gnbs, conf.AMF, ueScenarios) + r.StartScenario(gnbs, conf.AMF, ueScenarios, 500) time.Sleep(time.Duration(1000) * time.Millisecond) From 86e6544389f1c5717753b969e8ded2affbdb965e Mon Sep 17 00:00:00 2001 From: Raguideau Date: Thu, 11 Apr 2024 13:03:45 +0000 Subject: [PATCH 6/9] add unit tests for scenarioManager Signed-off-by: Raguideau --- cmd/packetrusher.go | 2 +- config/config.yml | 4 +- internal/common/tools/tools.go | 2 +- internal/control_test_engine/gnb/gnb.go | 74 +- internal/scenario/scenario.go | 8 +- internal/scenario/scenarioManager.go | 221 +++-- internal/scenario/scenarioManager_test.go | 865 ++++++++++++++++++ internal/scenario/task.go | 3 +- internal/scenario/ueTaskExecuter.go | 72 +- .../test-attach-gnb-with-configuration.go | 2 +- internal/templates/test-custom-scenario.go | 2 +- internal/templates/test-multi-ues-in-queue.go | 8 +- test/pr_test.go | 10 +- 13 files changed, 1069 insertions(+), 204 deletions(-) create mode 100644 internal/scenario/scenarioManager_test.go diff --git a/cmd/packetrusher.go b/cmd/packetrusher.go index 43f72d71..6fb49e39 100644 --- a/cmd/packetrusher.go +++ b/cmd/packetrusher.go @@ -98,7 +98,7 @@ func main() { &cli.IntFlag{Name: "timeBeforeIdle", Value: 0, Aliases: []string{"idl"}, Usage: "The time in ms, before switching UE to Idle. 0 to disable Idling."}, &cli.IntFlag{Name: "timeBeforeReconnecting", Value: 1000, Aliases: []string{"tbr"}, Usage: "The time in ms, before reconnecting to gNodeB after switching to Idle state. Default is 1000 ms. Only work in conjunction with timeBeforeIdle."}, &cli.IntFlag{Name: "numPduSessions", Value: 1, Aliases: []string{"nPdu"}, Usage: "The number of PDU Sessions to create"}, - &cli.BoolFlag{Name: "loop", Aliases: []string{"l"}, Usage: "Register UEs in a loop."}, + &cli.BoolFlag{Name: "loop", Aliases: []string{"l"}, Usage: "Loop over all procedures."}, &cli.BoolFlag{Name: "tunnel", Aliases: []string{"t"}, Usage: "Enable the creation of the GTP-U tunnel interface."}, &cli.BoolFlag{Name: "tunnel-vrf", Value: true, Usage: "Enable/disable VRP usage of the GTP-U tunnel interface."}, &cli.BoolFlag{Name: "dedicatedGnb", Aliases: []string{"d"}, Usage: "Enable the creation of a dedicated gNB per UE. Require one IP on N2/N3 per gNB."}, diff --git a/config/config.yml b/config/config.yml index af6400d4..9c1e2cf0 100644 --- a/config/config.yml +++ b/config/config.yml @@ -14,7 +14,7 @@ gnodeb: sst: "01" sd: "000001" # optional, can be removed if not used ue: - msin: "0000001000" + msin: "0000000120" key: "00112233445566778899AABBCCDDEEFF" opc: "00112233445566778899AABBCCDDEEFF" amf: "8000" @@ -41,4 +41,4 @@ amfif: - ip: "192.168.11.30" port: 38412 logs: - level: 4 \ No newline at end of file + level: 4 diff --git a/internal/common/tools/tools.go b/internal/common/tools/tools.go index 45f71d73..a6ed4695 100644 --- a/internal/common/tools/tools.go +++ b/internal/common/tools/tools.go @@ -38,7 +38,7 @@ func CreateGnbs(count int, cfg config.Config, wg *sync.WaitGroup) map[string]*gn cfg.GNodeB.ControlIF.Ip = n2Ip cfg.GNodeB.DataIF.Ip = n3Ip - gnbs[cfg.GNodeB.PlmnList.GnbId] = gnb.InitGnb(cfg, wg) + gnbs[cfg.GNodeB.PlmnList.GnbId] = gnb.InitGnb(cfg.GNodeB, cfg.AMFs, wg) wg.Add(1) // TODO: We could find the interfaces where N2/N3 are diff --git a/internal/control_test_engine/gnb/gnb.go b/internal/control_test_engine/gnb/gnb.go index ca2203a5..8a877261 100644 --- a/internal/control_test_engine/gnb/gnb.go +++ b/internal/control_test_engine/gnb/gnb.go @@ -20,58 +20,7 @@ import ( log "github.com/sirupsen/logrus" ) -func InitGnb(conf config.Config, wg *sync.WaitGroup) *context.GNBContext { - - // instance new gnb. - gnb := &context.GNBContext{} - - // new gnb context. - gnb.NewRanGnbContext( - conf.GNodeB.PlmnList.GnbId, - conf.GNodeB.PlmnList.Mcc, - conf.GNodeB.PlmnList.Mnc, - conf.GNodeB.PlmnList.Tac, - conf.GNodeB.SliceSupportList.Sst, - conf.GNodeB.SliceSupportList.Sd, - conf.GNodeB.ControlIF.Ip, - conf.GNodeB.DataIF.Ip, - conf.GNodeB.ControlIF.Port, - conf.GNodeB.DataIF.Port) - - // start communication with AMF (server SCTP). - for _, amfConfig := range conf.AMFs { - // new AMF context. - amf := gnb.NewGnBAmf(amfConfig.Ip, amfConfig.Port) - - // start communication with AMF(SCTP). - if err := ngap.InitConn(amf, gnb); err != nil { - log.Fatal("Error in", err) - } else { - log.Info("[GNB] SCTP/NGAP service is running") - // wg.Add(1) - } - - trigger.SendNgSetupRequest(gnb, amf) - } - - // start communication with UE (server UNIX sockets). - serviceNas.InitServer(gnb) - - go func() { - // control the signals - sigGnb := make(chan os.Signal, 1) - signal.Notify(sigGnb, os.Interrupt) - - // Block until a signal is received. - <-sigGnb - //gnb.Terminate() - wg.Done() - }() - - return gnb -} - -func InitGnb2(gnbConf config.GNodeB, amfConf config.AMF, wg *sync.WaitGroup) *context.GNBContext { +func InitGnb(gnbConf config.GNodeB, amfConfs []*config.AMF, wg *sync.WaitGroup) *context.GNBContext { // instance new gnb. gnb := &context.GNBContext{} @@ -90,23 +39,24 @@ func InitGnb2(gnbConf config.GNodeB, amfConf config.AMF, wg *sync.WaitGroup) *co gnbConf.DataIF.Port) // start communication with AMF (server SCTP). + for _, amfConfig := range amfConfs { + // new AMF context. + amf := gnb.NewGnBAmf(amfConfig.Ip, amfConfig.Port) - // new AMF context. - amf := gnb.NewGnBAmf(amfConf.Ip, amfConf.Port) + // start communication with AMF(SCTP). + if err := ngap.InitConn(amf, gnb); err != nil { + log.Fatal("Error in", err) + } else { + log.Info("[GNB] SCTP/NGAP service is running") + // wg.Add(1) + } - // start communication with AMF(SCTP). - if err := serviceNgap.InitConn(amf, gnb); err != nil { - log.Fatal("Error in", err) - } else { - log.Info("[GNB] SCTP/NGAP service is running") - // wg.Add(1) + trigger.SendNgSetupRequest(gnb, amf) } // start communication with UE (server UNIX sockets). serviceNas.InitServer(gnb) - trigger.SendNgSetupRequest(gnb, amf) - go func() { // control the signals sigGnb := make(chan os.Signal, 1) diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index b8b0e202..bf3c394e 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -10,8 +10,8 @@ import ( // Description of the scenario to be run for one UE type UEScenario struct { - Config config.Ue - Tasks []Task - Loop bool // Restart scenario once done - ForceStop int // Time before forcefully stoping scenario (0 to desactivate) + Config config.Ue + Tasks []Task + Loop bool // Restart scenario once done + Hang bool // Keep scenario going after all tasks are done } diff --git a/internal/scenario/scenarioManager.go b/internal/scenario/scenarioManager.go index 6aa75ce5..2c163a9d 100644 --- a/internal/scenario/scenarioManager.go +++ b/internal/scenario/scenarioManager.go @@ -5,6 +5,8 @@ package scenario import ( + "errors" + "fmt" "my5G-RANTester/config" "my5G-RANTester/internal/control_test_engine/gnb" "my5G-RANTester/internal/control_test_engine/gnb/context" @@ -16,12 +18,20 @@ import ( log "github.com/sirupsen/logrus" ) +var ( + InitGnb = gnb.InitGnb // using var to overide InitGnb function during tests +) + type ScenarioManager struct { gnbs map[string]*context.GNBContext ues map[int]*ueTasksCtx reportChan chan report registrationQueue chan order - wg *sync.WaitGroup // TODO: not used, to remove + gnbWg *sync.WaitGroup + taskExecuterWg *sync.WaitGroup + sigStop chan os.Signal + ueScenarios []UEScenario + stop bool } // Context of running ue scenario @@ -42,58 +52,94 @@ type order struct { workerChan chan Task } -func (s *ScenarioManager) StartScenario(gnbConfs []config.GNodeB, amfConf config.AMF, ueScenarios []UEScenario, timeBetweenRegistration int) { +func Start(gnbConfs []config.GNodeB, amfConfs []*config.AMF, ueScenarios []UEScenario, timeBetweenRegistration int, timeout int) { log.Info("------------------------------------ Starting scenario ------------------------------------") - s.wg = &sync.WaitGroup{} + s := initManager(ueScenarios, timeBetweenRegistration, timeout) + s.setupGnbs(gnbConfs, amfConfs) + s.setupScenarios(ueScenarios) + s.executeScenarios() + s.cleanup() + log.Info("------------------------------------ Scenario finished ------------------------------------") +} - s.reportChan = make(chan report, 10) +func initManager(ueScenarios []UEScenario, timeBetweenRegistration int, timeout int) *ScenarioManager { + log.Debug("Init manager with timeBetweenRegistration = ", timeBetweenRegistration) + s := &ScenarioManager{} + s.taskExecuterWg = &sync.WaitGroup{} + s.gnbWg = &sync.WaitGroup{} s.registrationQueue = make(chan order, len(ueScenarios)) - stopChan := make(chan int, 1) - go startRegistrationRateController(s.registrationQueue, timeBetweenRegistration, stopChan) - - s.setupGnbs(gnbConfs, amfConf) + go startOrderRateController(s.registrationQueue, timeBetweenRegistration) + s.reportChan = make(chan report, 10) s.ues = make(map[int]*ueTasksCtx) + s.sigStop = make(chan os.Signal, 1) + signal.Notify(s.sigStop, os.Interrupt) + if timeout > 0 { + time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() { + s.stop = true + }) + } + return s +} - sigStop := make(chan os.Signal, 1) - signal.Notify(sigStop, os.Interrupt) - - endSenario := false +func (s *ScenarioManager) setupScenarios(ueScenarios []UEScenario) { + s.stop = false + s.ueScenarios = ueScenarios for ueId := 1; ueId <= len(ueScenarios); ueId++ { - s.setupUeTaskExecuter(ueId, ueScenarios[ueId-1]) - + if err := s.setupUeTaskExecuter(ueId, ueScenarios[ueId-1]); err != nil { + log.Errorf("scenario for UE %d could not be started: %v", ueId, err) + } select { - case <-sigStop: - endSenario = true + case <-s.sigStop: + s.stop = true ueId = len(ueScenarios) + 1 // exit loop default: } } +} - for !endSenario { - endSenario = true +func (s *ScenarioManager) executeScenarios() { + for !s.stop { select { - case <-sigStop: - endSenario = true + case <-s.sigStop: + s.stop = true case report := <-s.reportChan: s.handleReport(report) - s.manageNextTask(report.ueId, ueScenarios) - for i := range s.ues { - if !s.ues[i].done { - endSenario = false + if err := s.manageNextTask(report.ueId, s.ueScenarios[report.ueId-1]); err != nil { + if !s.ueScenarios[report.ueId-1].Hang { + log.Debugf("Stopping simulation for ue %d, reason: %s", report.ueId, err) + s.ues[report.ueId].done = true + s.stop = true + for i := range s.ues { + if !s.ues[i].done { + s.stop = false + } + } } } } } +} + +func (s *ScenarioManager) cleanup() { + drainAndCloseOrderRateController(s.registrationQueue) + + go func() { + open := true + var report report + for open { + report, open = <-s.reportChan + log.Debug("Got report from UE ", report.ueId, " during cleanup: ", report.reason) + } + }() for _, ue := range s.ues { ue.workerChan <- Task{ TaskType: Terminate, } } - stopChan <- 0 - time.Sleep(time.Second * 3) - log.Info("------------------------------------ Scenario finished ------------------------------------") + s.taskExecuterWg.Wait() + close(s.reportChan) } func (s *ScenarioManager) handleReport(report report) { @@ -101,45 +147,59 @@ func (s *ScenarioManager) handleReport(report report) { if !report.success { s.ues[report.ueId].nextTasks = []Task{} log.Errorf("[UE-%d] Reported error: %s", report.ueId, report.reason) + return } log.Infof("[UE-%d] Reported succes: %s", report.ueId, report.reason) } -func (s *ScenarioManager) manageNextTask(ueId int, ueScenarios []UEScenario) bool { - newTask := false - if len(s.ues[ueId].nextTasks) > 0 { - s.sendNextTask(ueId) - return true +func (s *ScenarioManager) manageNextTask(ueId int, ueScenarios UEScenario) error { + var err error + if err = s.sendNextTask(ueId); err == nil { + return nil } - if ueScenarios[ueId-1].Loop { - s.restartUeScenario(ueId, ueScenarios[ueId-1]) - return true + if ueScenarios.Loop { + return s.restartUeScenario(ueId, ueScenarios) } - s.ues[ueId].done = true - return newTask + return err } -func (s *ScenarioManager) sendNextTask(ueId int) { - if s.ues[ueId].nextTasks[0].TaskType == Registration { - s.registrationQueue <- order{task: s.ues[ueId].nextTasks[0], workerChan: s.ues[ueId].workerChan} - } else { - s.ues[ueId].workerChan <- s.ues[ueId].nextTasks[0] +func (s *ScenarioManager) sendNextTask(ueId int) error { + _, exist := s.ues[ueId] + if !exist { + return errors.New("ue not found") } - s.ues[ueId].nextTasks = s.ues[ueId].nextTasks[1:] + if len(s.ues[ueId].nextTasks) > 0 { + if s.ues[ueId].nextTasks[0].TaskType == Registration { + s.registrationQueue <- order{task: s.ues[ueId].nextTasks[0], workerChan: s.ues[ueId].workerChan} + } else { + s.ues[ueId].workerChan <- s.ues[ueId].nextTasks[0] + } + s.ues[ueId].nextTasks = s.ues[ueId].nextTasks[1:] + return nil + } + return errors.New("ue does not have task to do") } -func (s *ScenarioManager) restartUeScenario(ueId int, ueScenario UEScenario) { +func (s *ScenarioManager) restartUeScenario(ueId int, ueScenario UEScenario) error { + _, exist := s.ues[ueId] + if !exist { + return errors.New("ue not found") + } + if ueScenario.Tasks == nil { + return fmt.Errorf("ue %d tasks is nil", ueId) + } log.Debugf("[UE-%d] starting new loop", ueId) s.ues[ueId].workerChan <- Task{TaskType: Kill} s.ues[ueId].nextTasks = ueScenario.Tasks + return nil } -func (s *ScenarioManager) setupGnbs(gnbConfs []config.GNodeB, amfConf config.AMF) { +func (s *ScenarioManager) setupGnbs(gnbConfs []config.GNodeB, amfConfs []*config.AMF) { s.gnbs = make(map[string]*context.GNBContext) for gnbConf := range gnbConfs { - s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = gnb.InitGnb2(gnbConfs[gnbConf], amfConf, s.wg) // TODO: replace InitGNB2 with InitGNB - s.wg.Add(1) + s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = InitGnb(gnbConfs[gnbConf], amfConfs, s.gnbWg) + s.gnbWg.Add(1) } // Wait for gNB to be connected before registering UEs @@ -147,53 +207,68 @@ func (s *ScenarioManager) setupGnbs(gnbConfs []config.GNodeB, amfConf config.AMF time.Sleep(2 * time.Second) } -func (s *ScenarioManager) setupUeTaskExecuter(ueId int, ueScenario UEScenario) { +func (s *ScenarioManager) setupUeTaskExecuter(ueId int, ueScenario UEScenario) error { + if ueId < 1 { + return errors.New("ueId must be at least 1") + } + _, exist := s.ues[ueId] + if exist { + return errors.New("this ue already have a task executer") + } + if ueScenario.Tasks == nil { + return errors.New("tasks list is nil") + } + if ueScenario.Config == (config.Ue{}) { + return errors.New("config is empty") + } + if s.reportChan == nil { + return errors.New("scenario manager's report channel is not set") + } + if s.gnbs == nil { + return errors.New("scenario manager's gnb list is not set") + } ue := ueTasksCtx{ done: false, nextTasks: ueScenario.Tasks, workerChan: make(chan Task, 1), } - if ueScenario.ForceStop > 0 { - time.AfterFunc(time.Duration(ueScenario.ForceStop)*time.Millisecond, func() { - ue.workerChan <- Task{TaskType: Terminate} - ue.done = true - }) - } - worker := ueTaskExecuter{ UeId: ueId, UeCfg: ueScenario.Config, TaskChan: ue.workerChan, ReportChan: s.reportChan, Gnbs: s.gnbs, - wg: s.wg, + Wg: s.taskExecuterWg, } worker.Run() s.ues[ueId] = &ue + return nil } -func startRegistrationRateController(orderChan chan order, timeBetweenRegistration int, stopChan chan int) { +func startOrderRateController(orderChan <-chan order, timeBetweenRegistration int) { waitTime := time.Duration(timeBetweenRegistration) * time.Millisecond - registrationTicker := time.NewTicker(waitTime) - log.Debug("Started registration rate controller") + orderTicker := time.NewTicker(waitTime) + var order order + var open bool for { - log.Debug("starting new register ticker loop") - select { - case <-registrationTicker.C: - log.Debug("registration rate controller tick! stoping ticker") - registrationTicker.Stop() - select { - case order := <-orderChan: - log.Debug("found register order, forwading task and resetting timer") - order.workerChan <- order.task - registrationTicker.Reset(waitTime) - case <-stopChan: - return - } - case <-stopChan: + <-orderTicker.C + orderTicker.Stop() + order, open = <-orderChan + if open { + order.workerChan <- order.task + orderTicker.Reset(waitTime) + } else { return } } } + +func drainAndCloseOrderRateController(orderChan chan order) { + close(orderChan) + open := true + for open { + _, open = <-orderChan + } +} diff --git a/internal/scenario/scenarioManager_test.go b/internal/scenario/scenarioManager_test.go new file mode 100644 index 00000000..1566e172 --- /dev/null +++ b/internal/scenario/scenarioManager_test.go @@ -0,0 +1,865 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * © Copyright 2024 Hewlett Packard Enterprise Development LP + */ +package scenario + +import ( + "io" + "my5G-RANTester/config" + "my5G-RANTester/internal/control_test_engine/gnb/context" + "os" + "sync" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +var defaultUeId int = 1 + +func TestMain(m *testing.M) { + log.SetOutput(io.Discard) + os.Exit(m.Run()) +} + +func getDefaultManager() ScenarioManager { + s := ScenarioManager{ + ues: map[int]*ueTasksCtx{}, + } + s.registrationQueue = make(chan order, 5) + ue := &ueTasksCtx{} + ue.workerChan = make(chan Task, 5) + ue.nextTasks = []Task{} + s.ues[defaultUeId] = ue + s.taskExecuterWg = &sync.WaitGroup{} + s.gnbWg = &sync.WaitGroup{} + return s +} + +// create gnb context without starting services +func gnbInitMock(gnbConf config.GNodeB, amfConfs []*config.AMF, wg *sync.WaitGroup) *context.GNBContext { + // instance new gnb. + gnb := &context.GNBContext{} + + // new gnb context. + gnb.NewRanGnbContext( + gnbConf.PlmnList.GnbId, + gnbConf.PlmnList.Mcc, + gnbConf.PlmnList.Mnc, + gnbConf.PlmnList.Tac, + gnbConf.SliceSupportList.Sst, + gnbConf.SliceSupportList.Sd, + gnbConf.ControlIF.Ip, + gnbConf.DataIF.Ip, + gnbConf.ControlIF.Port, + gnbConf.DataIF.Port) + + gnb.NewGnBAmf(amfConfs[0].Ip, amfConfs[0].Port) + + return gnb +} + +func TestHandleReport(t *testing.T) { + type input struct { + tasks []Task + report report + } + + type testCases struct { + description string + input input + expected []Task + } + + tasks := []Task{ + { + TaskType: Registration, + Delay: 2000, + }, + { + TaskType: Idle, + Delay: 500, + }, + } + + scenarios := []testCases{ + { + description: "successful report should not change tasks", + input: input{ + tasks: tasks, + report: report{ + ueId: defaultUeId, + success: true, + reason: "sucessful", + }, + }, + expected: tasks, + }, + { + description: "failed report should empty tasks list", + input: input{ + tasks: tasks, + report: report{ + ueId: defaultUeId, + success: false, + reason: "fail", + }, + }, + expected: []Task{}, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + s := &ScenarioManager{ + ues: map[int]*ueTasksCtx{defaultUeId: {nextTasks: tasks}}, + } + s.handleReport(scenario.input.report) + assert.Equal(t, scenario.expected, s.ues[defaultUeId].nextTasks) + }) + } +} + +func TestManageNextTask(t *testing.T) { + type input struct { + manager ScenarioManager + tasks []Task + UeScenario UEScenario + } + + type expected struct { + nextTasks []Task + err bool + } + + type testCases struct { + description string + input input + expected expected + } + + initialTasks := []Task{{ + TaskType: AttachToGNB, + Delay: 2000, + }, { + TaskType: Registration, + Delay: 2000, + }, { + TaskType: Idle, + Delay: 500, + }} + + scenarios := []testCases{{ + description: "Should send next task", + input: input{ + tasks: initialTasks, + manager: getDefaultManager(), + UeScenario: UEScenario{ + Tasks: initialTasks, + Loop: false, + }, + }, + expected: expected{ + nextTasks: []Task{{ + TaskType: Registration, + Delay: 2000, + }, { + TaskType: Idle, + Delay: 500, + }}, + err: false, + }, + }, { + description: "Should send next task without restarting scenario", + input: input{ + tasks: initialTasks, + manager: getDefaultManager(), + UeScenario: UEScenario{ + Tasks: initialTasks, + Loop: true, + }, + }, + expected: expected{ + nextTasks: []Task{{ + TaskType: Registration, + Delay: 2000, + }, { + TaskType: Idle, + Delay: 500, + }}, + err: false, + }, + }, { + description: "Should restart scenario", + input: input{ + tasks: []Task{}, + manager: getDefaultManager(), + UeScenario: UEScenario{ + Tasks: initialTasks, + Loop: true, + }, + }, + expected: expected{ + nextTasks: initialTasks, + err: false, + }, + }, { + description: "Should return error", + input: input{ + tasks: []Task{}, + manager: getDefaultManager(), + UeScenario: UEScenario{ + Tasks: initialTasks, + Loop: false, + }, + }, + expected: expected{ + nextTasks: []Task{}, + err: true, + }, + }} + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + s := scenario.input.manager + s.ues[defaultUeId].nextTasks = scenario.input.tasks + err := s.manageNextTask(defaultUeId, scenario.input.UeScenario) + assert.Equal(t, scenario.expected.nextTasks, s.ues[defaultUeId].nextTasks) + if scenario.expected.err { + assert.Error(t, err) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestSendNextTask(t *testing.T) { + type input struct { + manager ScenarioManager + ueid int + tasks []Task + } + + type expected struct { + nextTasks []Task + sentToWorker Task + sentToQueue order + err bool + } + + type testCases struct { + description string + input input + expected expected + } + + manager := getDefaultManager() + + scenarios := []testCases{ + { + description: "task is send to worker", + input: input{ + tasks: []Task{{ + TaskType: AttachToGNB, + Delay: 2000, + }, { + TaskType: Registration, + Delay: 2000, + }, { + TaskType: Idle, + Delay: 500, + }}, + manager: getDefaultManager(), + ueid: defaultUeId, + }, + expected: expected{ + nextTasks: []Task{{ + TaskType: Registration, + Delay: 2000, + }, { + TaskType: Idle, + Delay: 500, + }}, + sentToWorker: Task{ + TaskType: AttachToGNB, + Delay: 2000, + }, + sentToQueue: order{}, + err: false, + }, + }, + { + description: "registration task is send to queue for timer handling", + input: input{ + tasks: []Task{{ + TaskType: Registration, + Delay: 2000, + }, { + TaskType: Idle, + Delay: 500, + }}, + manager: manager, + ueid: defaultUeId, + }, + expected: expected{ + nextTasks: []Task{{ + TaskType: Idle, + Delay: 500, + }}, + sentToWorker: Task{}, + sentToQueue: order{ + workerChan: manager.ues[defaultUeId].workerChan, + task: Task{ + TaskType: Registration, + Delay: 2000, + }, + }, + err: false, + }, + }, + { + description: "Does not have task left", + input: input{ + tasks: []Task{}, + manager: getDefaultManager(), + ueid: defaultUeId, + }, + expected: expected{ + nextTasks: []Task{}, + sentToWorker: Task{}, + sentToQueue: order{}, + err: true, + }, + }, + { + description: "Ue not found", + input: input{ + tasks: []Task{}, + manager: getDefaultManager(), + ueid: 2, + }, + expected: expected{ + nextTasks: []Task{}, + sentToWorker: Task{}, + sentToQueue: order{}, + err: true, + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + + s := scenario.input.manager + s.ues[defaultUeId].nextTasks = scenario.input.tasks + res := s.sendNextTask(defaultUeId) + + var err string + + if scenario.expected.sentToWorker != (Task{}) { + select { + case task := <-s.ues[defaultUeId].workerChan: + assert.Equal(t, scenario.expected.sentToWorker, task) + default: + t.Error("Task expected to be sent to worker but was not.") + } + err = "More than one task was send to worker" + } else { + err = "Worker received a task but was not supposed to" + } + select { + case <-s.ues[defaultUeId].workerChan: + t.Error(err) + default: + } + + if scenario.expected.sentToQueue != (order{}) { + select { + case task := <-s.registrationQueue: + assert.Equal(t, scenario.expected.sentToQueue, task) + default: + t.Error("Task expected to be sent to registration queue but was not.") + } + err = "More than one task was send to registration queue" + } else { + err = "Registration queue received a task but was not supposed to" + } + select { + case <-s.registrationQueue: + t.Error(err) + default: + } + + if scenario.expected.err { + assert.Error(t, res) + } else { + assert.Nil(t, res, "SendNextTask returned an error but was not supposed to") + } + + assert.Equal(t, scenario.expected.nextTasks, s.ues[defaultUeId].nextTasks) + }) + } + +} + +func TestRestartUeScenario(t *testing.T) { + type input struct { + manager ScenarioManager + tasks []Task + } + + type testCases struct { + description string + input input + expected []Task + } + + tasks := []Task{{ + TaskType: AttachToGNB, + Delay: 0, + }, { + TaskType: Registration, + Delay: 2000, + }, { + TaskType: NewPDUSession, + Delay: 0, + }, { + TaskType: Idle, + Delay: 500, + }} + + scenarios := []testCases{{ + description: "UE should be killed and tasks reset", + input: input{ + manager: getDefaultManager(), + tasks: []Task{{ + TaskType: Idle, + Delay: 500, + }}, + }, + expected: tasks, + }, { + description: "UE should be killed and tasks reset", + input: input{ + manager: getDefaultManager(), + tasks: []Task{}, + }, + expected: tasks, + }} + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + s := scenario.input.manager + ueScenario := UEScenario{ + Tasks: tasks, + } + s.restartUeScenario(defaultUeId, ueScenario) + + select { + case task := <-s.ues[defaultUeId].workerChan: + assert.Equal(t, Task{TaskType: Kill}, task) + default: + t.Error("Kill Task expected to be sent to worker but was not.") + } + assert.Equal(t, scenario.expected, s.ues[defaultUeId].nextTasks) + }) + } +} + +func TestRestartUeScenarioHasErrors(t *testing.T) { + + type input struct { + manager ScenarioManager + tasks []Task + ueId int + } + + type testCases struct { + description string + input input + } + + scenarios := []testCases{{ + description: "Ue does not exist", + input: input{ + manager: getDefaultManager(), + tasks: []Task{}, + ueId: 2, + }}, { + description: "UeScenarios is nil", + input: input{ + manager: getDefaultManager(), + tasks: nil, + ueId: defaultUeId, + }, + }} + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + s := scenario.input.manager + ueScenario := UEScenario{ + Tasks: scenario.input.tasks, + } + assert.Error(t, s.restartUeScenario(scenario.input.ueId, ueScenario)) + }) + } +} + +func TestSetupGnbs(t *testing.T) { + type input struct { + manager ScenarioManager + gnbConfs []config.GNodeB + amfConfs []*config.AMF + } + + type testCases struct { + description string + input input + expected int + } + + gnbConfs1 := []config.GNodeB{ + { + ControlIF: config.ControlIF{ + Ip: "127.0.0.1", + Port: 9487, + }, + DataIF: config.DataIF{ + Ip: "127.0.0.2", + Port: 2152, + }, + PlmnList: config.PlmnList{ + Mcc: "999", + Mnc: "70", + Tac: "000001", + GnbId: "000001", + }, + SliceSupportList: config.SliceSupportList{ + Sst: "01", + }, + }, + } + + gnbConfs2 := []config.GNodeB{ + { + ControlIF: config.ControlIF{ + Ip: "192.168.11.12", + Port: 5000, + }, + DataIF: config.DataIF{ + Ip: "192.168.11.12", + Port: 3000, + }, + PlmnList: config.PlmnList{ + Mcc: "999", + Mnc: "70", + Tac: "000001", + GnbId: "000002", + }, + SliceSupportList: config.SliceSupportList{ + Sst: "01", + }, + }, + { + ControlIF: config.ControlIF{ + Ip: "192.168.11.13", + Port: 9487, + }, + DataIF: config.DataIF{ + Ip: "192.168.11.13", + Port: 2152, + }, + PlmnList: config.PlmnList{ + Mcc: "999", + Mnc: "70", + Tac: "000001", + GnbId: "000003", + }, + SliceSupportList: config.SliceSupportList{ + Sst: "01", + }, + }, + } + + amfConfs := []*config.AMF{{ + Ip: "192.168.11.30", + Port: 38412, + }} + + InitGnb = gnbInitMock + + scenarios := []testCases{{ + description: "1 gnb should be created", + input: input{ + manager: getDefaultManager(), + gnbConfs: gnbConfs1, + amfConfs: amfConfs, + }, + expected: len(gnbConfs1), + }, { + description: "2 gnbs should be created", + input: input{ + manager: getDefaultManager(), + gnbConfs: gnbConfs2, + amfConfs: amfConfs, + }, + expected: len(gnbConfs2), + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + s := scenario.input.manager + s.setupGnbs(scenario.input.gnbConfs, scenario.input.amfConfs) + for gnb := range scenario.input.gnbConfs { + gnbId := scenario.input.gnbConfs[gnb].PlmnList.GnbId + assert.Equal(t, gnbId, s.gnbs[gnbId].GetGnbId(), "GNB ID between key and values does not match") + } + assert.Equal(t, len(scenario.input.gnbConfs), len(s.gnbs), "Configured gnb and created gnb count does not match") + }) + } +} + +func TestSetupUeTaskExecuter(t *testing.T) { + type input struct { + manager ScenarioManager + ueScenario UEScenario + ueId int + gnbs *context.GNBContext + } + + type expected struct { + ueId int + config config.Ue + gnbs *context.GNBContext + tasks []Task + } + + type testCases struct { + description string + input input + expected expected + } + + gnbConf := config.GNodeB{ + ControlIF: config.ControlIF{ + Ip: "127.0.0.1", + Port: 9487, + }, + DataIF: config.DataIF{ + Ip: "127.0.0.2", + Port: 2152, + }, + PlmnList: config.PlmnList{ + Mcc: "999", + Mnc: "70", + Tac: "000001", + GnbId: "000001", + }, + SliceSupportList: config.SliceSupportList{ + Sst: "01", + }, + } + + amfConfs := []*config.AMF{{ + Ip: "192.168.11.30", + Port: 38412, + }} + + wg := &sync.WaitGroup{} + + gnbs := gnbInitMock(gnbConf, amfConfs, wg) + tasks := []Task{{ + TaskType: Registration, + Delay: 2000, + }, { + TaskType: NewPDUSession, + Delay: 0, + }, { + TaskType: Idle, + Delay: 0, + }} + + scenarios := []testCases{{ + description: "Test normal setup", + input: input{ + manager: ScenarioManager{ + ues: map[int]*ueTasksCtx{}, + }, + ueScenario: UEScenario{ + Config: config.Ue{ + Msin: "0000001000", + }, + Tasks: tasks, + }, + ueId: 1, + gnbs: gnbs, + }, + expected: expected{ + ueId: 1, + config: config.Ue{ + Msin: "0000001000", + }, + gnbs: gnbs, + tasks: tasks, + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.description, func(t *testing.T) { + s := scenario.input.manager + s.reportChan = make(chan report, 1) + s.gnbs = map[string]*context.GNBContext{scenario.input.gnbs.GetGnbId(): scenario.input.gnbs} + s.taskExecuterWg = &sync.WaitGroup{} + s.setupUeTaskExecuter(scenario.input.ueId, scenario.input.ueScenario) + + time.Sleep(time.Duration(100) * time.Millisecond) + + assert.Equal(t, scenario.expected.tasks, s.ues[scenario.input.ueId].nextTasks) + assert.Falsef(t, s.ues[scenario.input.ueId].done, "ueTasksCtx should not be done right after initialization") + select { + case report := <-s.reportChan: + assert.Equal(t, scenario.expected.ueId, report.ueId) + assert.Equal(t, "simulation started", report.reason) + default: + t.Error("no response from worker, there might be an issue with channels") + } + }) + } + +} + +func TestSetupUeTaskExecuterHasError(t *testing.T) { + + gnbConf := config.GNodeB{ + ControlIF: config.ControlIF{ + Ip: "127.0.0.1", + Port: 9487, + }, + DataIF: config.DataIF{ + Ip: "127.0.0.2", + Port: 2152, + }, + PlmnList: config.PlmnList{ + Mcc: "999", + Mnc: "70", + Tac: "000001", + GnbId: "000001", + }, + SliceSupportList: config.SliceSupportList{ + Sst: "01", + }, + } + + amfConfs := []*config.AMF{{ + Ip: "192.168.11.30", + Port: 38412, + }} + + wg := &sync.WaitGroup{} + + gnb := gnbInitMock(gnbConf, amfConfs, wg) + tasks := []Task{{ + TaskType: Registration, + Delay: 2000, + }, { + TaskType: NewPDUSession, + Delay: 0, + }, { + TaskType: Idle, + Delay: 0, + }} + ueScenario := UEScenario{ + Config: config.Ue{ + Msin: "0000001000", + }, + Tasks: tasks, + } + + t.Run("invalid UE Id", func(t *testing.T) { + + s := getDefaultManager() + s.reportChan = make(chan report, 1) + s.gnbs = map[string]*context.GNBContext{gnb.GetGnbId(): gnb} + s.taskExecuterWg = &sync.WaitGroup{} + ueid := 0 + assert.Error(t, s.setupUeTaskExecuter(ueid, ueScenario)) + }) + t.Run("Task Executer already exists", func(t *testing.T) { + + s := getDefaultManager() + s.reportChan = make(chan report, 1) + s.gnbs = map[string]*context.GNBContext{gnb.GetGnbId(): gnb} + s.taskExecuterWg = &sync.WaitGroup{} + ueid := 0 + s.setupUeTaskExecuter(ueid, ueScenario) + assert.Error(t, s.setupUeTaskExecuter(ueid, ueScenario)) + }) +} + +func TestStartOrderRateController(t *testing.T) { + orderCount := 20 + timeBetweenRegistration := 500 + orderChan := make(chan order, 5) + stopChan := make(chan int, 1) + taskChan := make(chan Task, 1) + go startOrderRateController(orderChan, timeBetweenRegistration) + + go func() { + for i := 0; i < orderCount; i++ { + orderChan <- order{task: Task{}, workerChan: taskChan} + } + }() + time.Sleep(time.Duration(timeBetweenRegistration/2) * time.Millisecond) + ticker := time.NewTicker(time.Duration(timeBetweenRegistration) * time.Millisecond) + endTicker := time.NewTicker(time.Duration((orderCount+5)*timeBetweenRegistration) * time.Millisecond) + expected := 0 + actual := 0 + stop := false + for !stop { + select { + case <-taskChan: + actual++ + case <-ticker.C: + if expected < orderCount { + expected++ + assert.Equal(t, expected, actual) + } + case <-endTicker.C: + stop = true + stopChan <- 0 + } + } + close(orderChan) + assert.Equal(t, orderCount, actual) +} + +func TestCleanup(t *testing.T) { + + s := getDefaultManager() + s.taskExecuterWg.Add(1) + time.AfterFunc(time.Duration(1)*time.Second, func() { s.taskExecuterWg.Done() }) + s.reportChan = make(chan report, 2) + s.reportChan <- report{ueId: 1, reason: "test"} + s.reportChan <- report{ueId: 2, reason: "test"} + + s.registrationQueue <- order{} + + s.cleanup() + + select { + case task := <-s.ues[1].workerChan: + assert.Equal(t, Terminate, task.TaskType) + default: + t.Error("Terminating task not send to UE") + } + + _, open := <-s.reportChan + assert.False(t, open, "report channel should be closed") + _, open = <-s.registrationQueue + assert.False(t, open, "registrationQueue channel should be closed") +} diff --git a/internal/scenario/task.go b/internal/scenario/task.go index e8653f49..832470c5 100644 --- a/internal/scenario/task.go +++ b/internal/scenario/task.go @@ -7,7 +7,6 @@ package scenario type Task struct { TaskType TaskType Delay int - Hang bool // hang simulation in this state until ctrl-c is received Parameters struct { GnbId string } @@ -20,13 +19,13 @@ const ( Registration Deregistration NewPDUSession - // DestroyPDUSession Terminate Kill Idle ServiceRequest NGAPHandover XNHandover + // DestroyPDUSession ) func (t TaskType) ToStr() string { diff --git a/internal/scenario/ueTaskExecuter.go b/internal/scenario/ueTaskExecuter.go index a7bfa139..5f756059 100644 --- a/internal/scenario/ueTaskExecuter.go +++ b/internal/scenario/ueTaskExecuter.go @@ -19,14 +19,19 @@ import ( log "github.com/sirupsen/logrus" ) +var ( + NewUe = ue.NewUE +) + type ueTaskExecuter struct { UeId int UeCfg config.Ue TaskChan chan Task ReportChan chan report Gnbs map[string]*context.GNBContext + Wg *sync.WaitGroup - wg *sync.WaitGroup + uewg *sync.WaitGroup running bool ueRx chan procedures.UeTesterMessage ueTx chan scenario.ScenarioMessage @@ -36,7 +41,6 @@ type ueTaskExecuter struct { state int targetState int lock sync.Mutex - toBeKilled chan procedures.UeTesterMessage loop bool } @@ -61,7 +65,7 @@ func (e *ueTaskExecuter) Run() { log.Errorf("simulation for ue %d failed: no gnbs", e.UeId) return } - if e.wg == nil { + if e.Wg == nil { log.Errorf("simulation for ue %d failed: no WaitGroup is given", e.UeId) return } @@ -72,6 +76,8 @@ func (e *ueTaskExecuter) Run() { e.state = ueCtx.MM5G_NULL e.targetState = ueCtx.MM5G_NULL e.lastReceivedTaskTime = time.Now() + e.Wg.Add(1) + e.uewg = &sync.WaitGroup{} go e.listen() } @@ -116,21 +122,11 @@ func (e *ueTaskExecuter) handleTaskTimer(task Task) { } func (e *ueTaskExecuter) handleTask(task Task) { - if log.GetLevel() >= 5 { - log.Debugf("------------------------------------ Task UE-%d ------------------------------------", e.UeId) - log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) - log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) - for i := range e.Gnbs { - for j := 1; j <= 3; j++ { - _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(j)) - if err == nil { - log.Debugf("GNB %s has ue %d context", e.Gnbs[i].GetGnbId(), j) - } - } - } - } + log.Debugf("------------------------------------ Task UE-%d ------------------------------------", e.UeId) + log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) + log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) - if e.attachedGnb == "" && task.TaskType != AttachToGNB { + if e.attachedGnb == "" && task.TaskType != AttachToGNB && task.TaskType != Terminate { msg := fmt.Sprintf("UE %d is not attached to a GNB", e.UeId) e.sendReport(false, msg) } @@ -171,8 +167,9 @@ func (e *ueTaskExecuter) handleAttachToGnb(task Task) { // ue.NewUE returns context of the new UE e.lock.Lock() e.ueRx = make(chan procedures.UeTesterMessage) - e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.wg) - e.wg.Add(1) + log.Debug("add wg") + e.uewg.Add(1) + e.ueTx = ue.NewUE(e.UeCfg, e.UeId, e.ueRx, e.Gnbs[task.Parameters.GnbId].GetInboundChannel(), e.uewg) e.attachedGnb = task.Parameters.GnbId e.lock.Unlock() e.sendReport(true, "UE successfully attached to GNB "+task.Parameters.GnbId) @@ -183,9 +180,6 @@ func (e *ueTaskExecuter) handleAttachToGnb(task Task) { } func (e *ueTaskExecuter) handleRegistration() { - if e.toBeKilled != nil { - e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Kill} - } e.ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} e.targetState = ueCtx.MM5G_REGISTERED } @@ -208,21 +202,10 @@ func (e *ueTaskExecuter) handleServiceRequest() { func (e *ueTaskExecuter) handleNewPduSession(task Task) { if e.state == ueCtx.MM5G_REGISTERED { - log.Debugf("UE %d Before PDU session creation", e.UeId) - for i := range e.Gnbs { - _, err := e.Gnbs[i].GetGnbUeByPrUeId(int64(e.UeId)) - if err == nil { - log.Debugf("GNB %s has ue %d", e.Gnbs[i].GetGnbId(), e.UeId) - } else { - log.Debugf("GNB %s does not have %d: %s", e.Gnbs[i].GetGnbId(), e.UeId, err) - } - } e.ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} - if !task.Hang { - // TODO: Implement way to check if pduSession is successful - time.Sleep(2 * time.Second) - e.sendReport(true, "sent PDU session request") - } + // TODO: Implement way to check if pduSession is successful + time.Sleep(2 * time.Second) + e.sendReport(true, "sent PDU session request") } else { msg := fmt.Sprintf("UE %d is not registered", e.UeId) e.sendReport(false, msg) @@ -260,24 +243,21 @@ func (e *ueTaskExecuter) handleNgapHandover(task Task) { } func (e *ueTaskExecuter) handleTerminate() { - if e.toBeKilled != nil { - e.toBeKilled <- procedures.UeTesterMessage{Type: procedures.Terminate} - e.lock.Lock() - e.toBeKilled = nil - e.lock.Unlock() - } e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} + open := true + for open { + _, open = <-e.ueTx + } e.reset() + e.uewg.Wait() e.sendReport(true, "UE Terminated") close(e.TaskChan) e.loop = false + e.Wg.Done() } func (e *ueTaskExecuter) handleKill() { - // Not killing immediately to be able to deregister ue when receiving terminate command right after killing command - e.lock.Lock() - e.toBeKilled = e.ueRx - e.lock.Unlock() + e.ueRx <- procedures.UeTesterMessage{Type: procedures.Kill} e.reset() e.sendReport(true, "UE Killed") } diff --git a/internal/templates/test-attach-gnb-with-configuration.go b/internal/templates/test-attach-gnb-with-configuration.go index 0e704fc2..556f999f 100644 --- a/internal/templates/test-attach-gnb-with-configuration.go +++ b/internal/templates/test-attach-gnb-with-configuration.go @@ -23,7 +23,7 @@ func TestAttachGnbWithConfiguration() { // cfg.GNodeB.SliceSupportList.St = "10" // cfg.GNodeB.SliceSupportList.Sst = "010239" - go gnb.InitGnb(cfg, &wg) + go gnb.InitGnb(cfg.GNodeB, cfg.AMFs, &wg) wg.Add(1) diff --git a/internal/templates/test-custom-scenario.go b/internal/templates/test-custom-scenario.go index 586aa5d7..38db6f95 100644 --- a/internal/templates/test-custom-scenario.go +++ b/internal/templates/test-custom-scenario.go @@ -25,7 +25,7 @@ func TestWithCustomScenario(scenarioPath string) { wg.Add(1) - gnb := gnb.InitGnb(cfg, &wg) + gnb := gnb.InitGnb(cfg.GNodeB, cfg.AMFs, &wg) time.Sleep(1 * time.Second) diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index 020cb276..25c500b2 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -118,15 +118,13 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb Tasks: tasks, } - if loop { - ueScenario.Loop = loop - } + ueScenario.Loop = loop + ueScenario.Hang = true ueScenarios = append(ueScenarios, ueScenario) } - r := scenario.ScenarioManager{} - r.StartScenario(gnbs, cfg.AMF, ueScenarios, timeBetweenRegistration) + scenario.Start(gnbs, cfg.AMFs, ueScenarios, timeBetweenRegistration, 0) } func nextGnbConf(gnb config.GNodeB, i int, baseId string) config.GNodeB { diff --git a/test/pr_test.go b/test/pr_test.go index 53b6f7d0..cef278b2 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -74,9 +74,8 @@ func TestSingleUe(t *testing.T) { gnbs := []config.GNodeB{conf.GNodeB} ueScenario := scenario.UEScenario{ - Config: conf.Ue, - Loop: false, - ForceStop: 20000, + Config: conf.Ue, + Loop: false, Tasks: []scenario.Task{ { TaskType: scenario.AttachToGNB, @@ -98,10 +97,9 @@ func TestSingleUe(t *testing.T) { } ueScenarios := []scenario.UEScenario{ueScenario} - r := scenario.ScenarioManager{} - r.StartScenario(gnbs, conf.AMF, ueScenarios, 500) + scenario.Start(gnbs, conf.AMF, ueScenarios, 500, 30000) - time.Sleep(time.Duration(1000) * time.Millisecond) + time.Sleep(time.Duration(4000) * time.Millisecond) fiveGC.GetAMFContext().ExecuteForAllUe( func(ue *context.UEContext) { From 9f9c89566bc5d81435bd3c4299c29f64ff612604 Mon Sep 17 00:00:00 2001 From: Raguideau Date: Wed, 17 Apr 2024 14:29:45 +0000 Subject: [PATCH 7/9] update tests Signed-off-by: Raguideau --- internal/scenario/scenarioManager.go | 4 +- internal/scenario/ueTaskExecuter.go | 1 - internal/templates/test-multi-ues-in-queue.go | 4 +- test/aio5gc/context/sessionManagement.go | 2 +- test/pr_test.go | 164 +++++++++--------- 5 files changed, 85 insertions(+), 90 deletions(-) diff --git a/internal/scenario/scenarioManager.go b/internal/scenario/scenarioManager.go index 2c163a9d..9a9a6e82 100644 --- a/internal/scenario/scenarioManager.go +++ b/internal/scenario/scenarioManager.go @@ -75,7 +75,8 @@ func initManager(ueScenarios []UEScenario, timeBetweenRegistration int, timeout signal.Notify(s.sigStop, os.Interrupt) if timeout > 0 { time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() { - s.stop = true + log.Debug("Scenario Timeout, sending interrupt signal") + s.sigStop <- os.Interrupt }) } return s @@ -143,7 +144,6 @@ func (s *ScenarioManager) cleanup() { } func (s *ScenarioManager) handleReport(report report) { - log.Debugf("------------------------------------ Report UE-%d ------------------------------------", report.ueId) if !report.success { s.ues[report.ueId].nextTasks = []Task{} log.Errorf("[UE-%d] Reported error: %s", report.ueId, report.reason) diff --git a/internal/scenario/ueTaskExecuter.go b/internal/scenario/ueTaskExecuter.go index 5f756059..03c0a2da 100644 --- a/internal/scenario/ueTaskExecuter.go +++ b/internal/scenario/ueTaskExecuter.go @@ -122,7 +122,6 @@ func (e *ueTaskExecuter) handleTaskTimer(task Task) { } func (e *ueTaskExecuter) handleTask(task Task) { - log.Debugf("------------------------------------ Task UE-%d ------------------------------------", e.UeId) log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index 25c500b2..e3b251e8 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -119,7 +119,9 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb } ueScenario.Loop = loop - ueScenario.Hang = true + if tasks[len(tasks)-1].TaskType != scenario.Deregistration { + ueScenario.Hang = true + } ueScenarios = append(ueScenarios, ueScenario) } diff --git a/test/aio5gc/context/sessionManagement.go b/test/aio5gc/context/sessionManagement.go index 96e4ea0a..33c327b6 100644 --- a/test/aio5gc/context/sessionManagement.go +++ b/test/aio5gc/context/sessionManagement.go @@ -228,6 +228,6 @@ func ForceReleaseAllPDUSession(ue *UEContext) { if err != nil { log.Error("[5GC] Failed to release pdu session " + fmt.Sprint(smCtx.GetPduSessionId()) + " for ue " + ue.securityContext.msin + ": " + err.Error()) } - ue.DeleteSmContext(smCtx.GetPduSessionId()) + delete(ue.smContexts, smCtx.GetPduSessionId()) }) } diff --git a/test/pr_test.go b/test/pr_test.go index cef278b2..f997219f 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -8,13 +8,11 @@ package test import ( "my5G-RANTester/config" "my5G-RANTester/internal/common/tools" - "my5G-RANTester/internal/control_test_engine/procedures" "my5G-RANTester/internal/scenario" "my5G-RANTester/test/aio5gc" "my5G-RANTester/test/aio5gc/context" amfTools "my5G-RANTester/test/aio5gc/lib/tools" "os" - "sync" "testing" "time" @@ -33,10 +31,10 @@ func TestSingleUe(t *testing.T) { Ip: "127.0.0.1", Port: 2154, } - amfConfig := config.AMF{ + amfConfig := []*config.AMF{{ Ip: "127.0.0.1", Port: 38414, - } + }} conf := amfTools.GenerateDefaultConf(controlIFConfig, dataIFConfig, amfConfig) type UECheck struct { @@ -76,6 +74,7 @@ func TestSingleUe(t *testing.T) { ueScenario := scenario.UEScenario{ Config: conf.Ue, Loop: false, + Hang: false, Tasks: []scenario.Task{ { TaskType: scenario.AttachToGNB, @@ -97,9 +96,9 @@ func TestSingleUe(t *testing.T) { } ueScenarios := []scenario.UEScenario{ueScenario} - scenario.Start(gnbs, conf.AMF, ueScenarios, 500, 30000) + scenario.Start(gnbs, conf.AMFs, ueScenarios, 50, 30000) - time.Sleep(time.Duration(4000) * time.Millisecond) + time.Sleep(2 * time.Second) fiveGC.GetAMFContext().ExecuteForAllUe( func(ue *context.UEContext) { @@ -163,50 +162,52 @@ func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { } time.Sleep(1 * time.Second) - // Setup gNodeB - gnbCount := 1 - wg := sync.WaitGroup{} - gnbs := tools.CreateGnbs(gnbCount, conf, &wg) - - time.Sleep(1 * time.Second) - - keys := make([]string, 0) - for k := range gnbs { - keys = append(keys, k) - } - // Setup UE ueCount := 10 - scenarioChans := make([]chan procedures.UeTesterMessage, ueCount+1) - ueSimCfg := tools.UESimulationConfig{ - Gnbs: gnbs, - Cfg: conf, - TimeBeforeDeregistration: 400, - TimeBeforeNgapHandover: 0, - TimeBeforeXnHandover: 0, - NumPduSessions: 1, - } - - for ueSimCfg.UeId = 1; ueSimCfg.UeId <= ueCount; ueSimCfg.UeId++ { - ueSimCfg.ScenarioChan = scenarioChans[ueSimCfg.UeId] - - imsi := tools.IncrementMsin(ueSimCfg.UeId, ueSimCfg.Cfg.Ue.Msin) + amfContext := fiveGC.GetAMFContext() + ueScenarios := []scenario.UEScenario{} + for i := 1; i <= ueCount; i++ { + tmpConf := conf + tmpConf.Ue.Msin = tools.IncrementMsin(i, conf.Ue.Msin) + ueScenario := scenario.UEScenario{ + Config: tmpConf.Ue, + Loop: false, + Hang: false, + Tasks: []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{tmpConf.GNodeB.PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + { + TaskType: scenario.NewPDUSession, + }, + { + TaskType: scenario.Deregistration, + Delay: 2000, + }, + }, + } + ueScenarios = append(ueScenarios, ueScenario) securityContext := context.SecurityContext{} - securityContext.SetMsin(imsi) - securityContext.SetAuthSubscription(ueSimCfg.Cfg.Ue.Key, ueSimCfg.Cfg.Ue.Opc, "c9e8763286b5b9ffbdf56e1297d0887b", conf.Ue.Amf, conf.Ue.Sqn) + securityContext.SetMsin(tmpConf.Ue.Msin) + securityContext.SetAuthSubscription(tmpConf.Ue.Key, tmpConf.Ue.Opc, "c9e8763286b5b9ffbdf56e1297d0887b", conf.Ue.Amf, conf.Ue.Sqn) securityContext.SetAbba([]uint8{0x00, 0x00}) - amfContext := fiveGC.GetAMFContext() - amfContext.Provision(models.Snssai{Sst: int32(ueSimCfg.Cfg.Ue.Snssai.Sst), Sd: ueSimCfg.Cfg.Ue.Snssai.Sd}, securityContext) + amfContext.Provision(models.Snssai{Sst: int32(tmpConf.Ue.Snssai.Sst), Sd: tmpConf.Ue.Snssai.Sd}, securityContext) - tools.SimulateSingleUE(ueSimCfg, &wg) - - // Before creating a new UE, we wait for 5 ms - time.Sleep(time.Duration(5) * time.Millisecond) } - time.Sleep(time.Duration(5000) * time.Millisecond) + gnbs := []config.GNodeB{conf.GNodeB} + scenario.Start(gnbs, conf.AMFs, ueScenarios, 500, 40000) + + time.Sleep(2 * time.Second) + i := 0 fiveGC.GetAMFContext().ExecuteForAllUe( func(ue *context.UEContext) { @@ -267,58 +268,51 @@ func TestUERegistrationLoop(t *testing.T) { } time.Sleep(1 * time.Second) - // Setup gNodeB - gnbCount := 1 - wg := sync.WaitGroup{} - gnbs := tools.CreateGnbs(gnbCount, conf, &wg) - - time.Sleep(1 * time.Second) - - keys := make([]string, 0) - for k := range gnbs { - keys = append(keys, k) - } - // Setup UE - loopCount := 5 - scenarioChans := make([]chan procedures.UeTesterMessage, 2) - ueSimCfg := tools.UESimulationConfig{ - UeId: 1, - Gnbs: gnbs, - Cfg: conf, - TimeBeforeDeregistration: 3000, - TimeBeforeNgapHandover: 0, - TimeBeforeXnHandover: 0, - NumPduSessions: 1, - } - - for i := 0; i < loopCount; i++ { - if scenarioChans[ueSimCfg.UeId] != nil { - scenarioChans[ueSimCfg.UeId] <- procedures.UeTesterMessage{Type: procedures.Kill} - close(scenarioChans[ueSimCfg.UeId]) - scenarioChans[ueSimCfg.UeId] = nil - } - scenarioChans[ueSimCfg.UeId] = make(chan procedures.UeTesterMessage) - ueSimCfg.ScenarioChan = scenarioChans[ueSimCfg.UeId] - - securityContext := context.SecurityContext{} - securityContext.SetMsin(tools.IncrementMsin(ueSimCfg.UeId, ueSimCfg.Cfg.Ue.Msin)) - securityContext.SetAuthSubscription(ueSimCfg.Cfg.Ue.Key, ueSimCfg.Cfg.Ue.Opc, "c9e8763286b5b9ffbdf56e1297d0887b", conf.Ue.Amf, conf.Ue.Sqn) - securityContext.SetAbba([]uint8{0x00, 0x00}) - - amfContext := fiveGC.GetAMFContext() - amfContext.Provision(models.Snssai{Sst: int32(ueSimCfg.Cfg.Ue.Snssai.Sst), Sd: ueSimCfg.Cfg.Ue.Snssai.Sd}, securityContext) + securityContext := context.SecurityContext{} + securityContext.SetMsin(conf.Ue.Msin) + securityContext.SetAuthSubscription(conf.Ue.Key, conf.Ue.Opc, "c9e8763286b5b9ffbdf56e1297d0887b", conf.Ue.Amf, conf.Ue.Sqn) + securityContext.SetAbba([]uint8{0x00, 0x00}) - tools.SimulateSingleUE(ueSimCfg, &wg) + amfContext := fiveGC.GetAMFContext() + amfContext.Provision(models.Snssai{Sst: int32(conf.Ue.Snssai.Sst), Sd: conf.Ue.Snssai.Sd}, securityContext) - // Before creating a new UE, we wait for 2000 ms - time.Sleep(time.Duration(2000) * time.Millisecond) + gnbs := []config.GNodeB{conf.GNodeB} + ueScenario := scenario.UEScenario{ + Config: conf.Ue, + Loop: true, + Hang: false, + Tasks: []scenario.Task{ + { + TaskType: scenario.AttachToGNB, + Parameters: struct { + GnbId string + }{conf.GNodeB.PlmnList.GnbId}, + }, + { + TaskType: scenario.Registration, + }, + { + TaskType: scenario.NewPDUSession, + }, + { + TaskType: scenario.Deregistration, + Delay: 2000, + }, + }, } + ueScenarios := []scenario.UEScenario{ueScenario} + + scenario.Start(gnbs, conf.AMFs, ueScenarios, 50, 15000) - time.Sleep(time.Duration(5000) * time.Millisecond) + time.Sleep(2 * time.Second) + i := 0 fiveGC.GetAMFContext().ExecuteForAllUe( func(ue *context.UEContext) { + i++ assert.Equalf(t, context.Deregistered, ue.GetState().Current(), "Expected all ue to be in Deregistered state but was not") assert.True(t, ueChecks[ue.GetSecurityContext().GetMsin()].HasAuthOnce, "UE has never changed state") + }) + assert.GreaterOrEqual(t, i, 3) } From 1927a958248e78ce0ee22d1ad2f113b9e336231e Mon Sep 17 00:00:00 2001 From: Raguideau Date: Thu, 18 Apr 2024 08:31:45 +0000 Subject: [PATCH 8/9] fix tests parallelization Signed-off-by: Raguideau --- test/aio5gc/lib/tools/tools.go | 59 ---------------- test/pr_test.go | 124 +++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 105 deletions(-) delete mode 100644 test/aio5gc/lib/tools/tools.go diff --git a/test/aio5gc/lib/tools/tools.go b/test/aio5gc/lib/tools/tools.go deleted file mode 100644 index 88654320..00000000 --- a/test/aio5gc/lib/tools/tools.go +++ /dev/null @@ -1,59 +0,0 @@ -/** - * SPDX-License-Identifier: Apache-2.0 - * © Copyright 2023 Hewlett Packard Enterprise Development LP - */ -package tools - -import ( - "my5G-RANTester/config" -) - -func GenerateDefaultConf(controlIF config.ControlIF, dataIF config.DataIF, amfs []*config.AMF) config.Config { - return config.Config{ - GNodeB: config.GNodeB{ - ControlIF: controlIF, - DataIF: dataIF, - PlmnList: config.PlmnList{ - Mcc: "999", - Mnc: "70", - Tac: "000001", - GnbId: "000008", - }, - SliceSupportList: config.SliceSupportList{ - Sst: "01", - Sd: "000001", - }, - }, - Ue: config.Ue{ - Msin: "0000000120", - Key: "00112233445566778899AABBCCDDEEFF", - Opc: "00112233445566778899AABBCCDDEEFF", - Amf: "8000", - Sqn: "00000000", - Dnn: "internet", - RoutingIndicator: "4567", - Hplmn: config.Hplmn{ - Mcc: "999", - Mnc: "70", - }, - Snssai: config.Snssai{ - Sst: 01, - Sd: "000001", - }, - Integrity: config.Integrity{ - Nia0: false, - Nia1: true, - Nia2: true, - }, - Ciphering: config.Ciphering{ - Nea0: false, - Nea1: true, - Nea2: true, - }, - }, - AMFs: amfs, - Logs: config.Logs{ - Level: 4, - }, - } -} diff --git a/test/pr_test.go b/test/pr_test.go index f997219f..15e20eb1 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -11,8 +11,8 @@ import ( "my5G-RANTester/internal/scenario" "my5G-RANTester/test/aio5gc" "my5G-RANTester/test/aio5gc/context" - amfTools "my5G-RANTester/test/aio5gc/lib/tools" "os" + "sync" "testing" "time" @@ -22,21 +22,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSingleUe(t *testing.T) { - controlIFConfig := config.ControlIF{ - Ip: "127.0.0.1", - Port: 9489, - } - dataIFConfig := config.DataIF{ - Ip: "127.0.0.1", - Port: 2154, - } - amfConfig := []*config.AMF{{ - Ip: "127.0.0.1", - Port: 38414, - }} +var ( + testCount = 0 + mutex sync.Mutex +) - conf := amfTools.GenerateDefaultConf(controlIFConfig, dataIFConfig, amfConfig) +func TestSingleUe(t *testing.T) { + conf := generateDefaultConf() type UECheck struct { HasAuthOnce bool } @@ -109,22 +101,7 @@ func TestSingleUe(t *testing.T) { func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { - controlIFConfig := config.ControlIF{ - Ip: "127.0.0.1", - Port: 9489, - } - dataIFConfig := config.DataIF{ - Ip: "127.0.0.1", - Port: 2154, - } - amfListConfig := []*config.AMF{ - { - Ip: "127.0.0.1", - Port: 38414, - }, - } - - conf := amfTools.GenerateDefaultConf(controlIFConfig, dataIFConfig, amfListConfig) + conf := generateDefaultConf() type UECheck struct { HasAuthOnce bool @@ -226,27 +203,13 @@ func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { } func TestUERegistrationLoop(t *testing.T) { - controlIFConfig := config.ControlIF{ - Ip: "127.0.0.1", - Port: 9490, - } - dataIFConfig := config.DataIF{ - Ip: "127.0.0.1", - Port: 2155, - } - amfListConfig := []*config.AMF{ - { - Ip: "127.0.0.1", - Port: 38415, - }, - } type UECheck struct { HasAuthOnce bool } ueChecks := map[string]*UECheck{} - conf := amfTools.GenerateDefaultConf(controlIFConfig, dataIFConfig, amfListConfig) + conf := generateDefaultConf() // Setup 5GC builder := aio5gc.FiveGCBuilder{} @@ -316,3 +279,72 @@ func TestUERegistrationLoop(t *testing.T) { }) assert.GreaterOrEqual(t, i, 3) } + +func generateDefaultConf() config.Config { + + conf := config.Config{ + GNodeB: config.GNodeB{ + ControlIF: config.ControlIF{ + Ip: "127.0.0.1", + Port: 9489, + }, + DataIF: config.DataIF{ + Ip: "127.0.0.1", + Port: 2154, + }, + PlmnList: config.PlmnList{ + Mcc: "999", + Mnc: "70", + Tac: "000001", + GnbId: "000008", + }, + SliceSupportList: config.SliceSupportList{ + Sst: "01", + Sd: "000001", + }, + }, + Ue: config.Ue{ + Msin: "0000000120", + Key: "00112233445566778899AABBCCDDEEFF", + Opc: "00112233445566778899AABBCCDDEEFF", + Amf: "8000", + Sqn: "00000000", + Dnn: "internet", + RoutingIndicator: "4567", + Hplmn: config.Hplmn{ + Mcc: "999", + Mnc: "70", + }, + Snssai: config.Snssai{ + Sst: 01, + Sd: "000001", + }, + Integrity: config.Integrity{ + Nia0: false, + Nia1: true, + Nia2: true, + }, + Ciphering: config.Ciphering{ + Nea0: false, + Nea1: true, + Nea2: true, + }, + }, + AMFs: []*config.AMF{ + { + Ip: "127.0.0.1", + Port: 38414, + }}, + Logs: config.Logs{ + Level: 4, + }, + } + + mutex.Lock() + conf.AMFs[0].Port = conf.AMFs[0].Port + testCount + conf.GNodeB.ControlIF.Port = conf.GNodeB.ControlIF.Port + testCount + conf.GNodeB.DataIF.Port = conf.GNodeB.DataIF.Port + testCount + testCount++ + mutex.Unlock() + return conf +} From c3bbed8ceab1cbfb9304d212a441d60ad17cf0c7 Mon Sep 17 00:00:00 2001 From: Raguideau Date: Thu, 18 Apr 2024 09:59:24 +0000 Subject: [PATCH 9/9] address pr comments Signed-off-by: Raguideau --- internal/scenario/scenario.go | 8 +- internal/scenario/scenarioManager.go | 197 +++++++++--------- internal/scenario/scenarioManager_test.go | 26 +-- internal/scenario/ueTaskExecuter.go | 38 ++-- internal/templates/test-multi-ues-in-queue.go | 8 +- test/pr_test.go | 18 +- 6 files changed, 147 insertions(+), 148 deletions(-) diff --git a/internal/scenario/scenario.go b/internal/scenario/scenario.go index bf3c394e..b658cbfd 100644 --- a/internal/scenario/scenario.go +++ b/internal/scenario/scenario.go @@ -10,8 +10,8 @@ import ( // Description of the scenario to be run for one UE type UEScenario struct { - Config config.Ue - Tasks []Task - Loop bool // Restart scenario once done - Hang bool // Keep scenario going after all tasks are done + Config config.Ue + Tasks []Task + Loop bool // Restart scenario once done + Persist bool // Keep scenario going after all tasks are done } diff --git a/internal/scenario/scenarioManager.go b/internal/scenario/scenarioManager.go index 9a9a6e82..1913d8e9 100644 --- a/internal/scenario/scenarioManager.go +++ b/internal/scenario/scenarioManager.go @@ -28,7 +28,7 @@ type ScenarioManager struct { reportChan chan report registrationQueue chan order gnbWg *sync.WaitGroup - taskExecuterWg *sync.WaitGroup + taskExecutorWg *sync.WaitGroup sigStop chan os.Signal ueScenarios []UEScenario stop bool @@ -65,12 +65,17 @@ func Start(gnbConfs []config.GNodeB, amfConfs []*config.AMF, ueScenarios []UESce func initManager(ueScenarios []UEScenario, timeBetweenRegistration int, timeout int) *ScenarioManager { log.Debug("Init manager with timeBetweenRegistration = ", timeBetweenRegistration) s := &ScenarioManager{} - s.taskExecuterWg = &sync.WaitGroup{} + + s.ues = make(map[int]*ueTasksCtx) + + s.taskExecutorWg = &sync.WaitGroup{} s.gnbWg = &sync.WaitGroup{} + + s.reportChan = make(chan report, 10) + s.registrationQueue = make(chan order, len(ueScenarios)) go startOrderRateController(s.registrationQueue, timeBetweenRegistration) - s.reportChan = make(chan report, 10) - s.ues = make(map[int]*ueTasksCtx) + s.sigStop = make(chan os.Signal, 1) signal.Notify(s.sigStop, os.Interrupt) if timeout > 0 { @@ -82,22 +87,87 @@ func initManager(ueScenarios []UEScenario, timeBetweenRegistration int, timeout return s } +func startOrderRateController(orderChan <-chan order, timeBetweenRegistration int) { + waitTime := time.Duration(timeBetweenRegistration) * time.Millisecond + orderTicker := time.NewTicker(waitTime) + var order order + var open bool + for { + <-orderTicker.C + orderTicker.Stop() + order, open = <-orderChan + if open { + order.workerChan <- order.task + orderTicker.Reset(waitTime) + } else { + return + } + } +} + +func (s *ScenarioManager) setupGnbs(gnbConfs []config.GNodeB, amfConfs []*config.AMF) { + s.gnbs = make(map[string]*context.GNBContext) + + for gnbConf := range gnbConfs { + s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = InitGnb(gnbConfs[gnbConf], amfConfs, s.gnbWg) + s.gnbWg.Add(1) + } + + // Wait for gNB to be connected before registering UEs + // TODO: We should wait for NGSetupResponse instead + time.Sleep(2 * time.Second) +} + func (s *ScenarioManager) setupScenarios(ueScenarios []UEScenario) { s.stop = false s.ueScenarios = ueScenarios for ueId := 1; ueId <= len(ueScenarios); ueId++ { - if err := s.setupUeTaskExecuter(ueId, ueScenarios[ueId-1]); err != nil { + if err := s.setupUeTaskExecutor(ueId, ueScenarios[ueId-1]); err != nil { log.Errorf("scenario for UE %d could not be started: %v", ueId, err) } - select { - case <-s.sigStop: - s.stop = true - ueId = len(ueScenarios) + 1 // exit loop - default: - } } } +func (s *ScenarioManager) setupUeTaskExecutor(ueId int, ueScenario UEScenario) error { + if ueId < 1 { + return errors.New("ueId must be at least 1") + } + _, exist := s.ues[ueId] + if exist { + return errors.New("this ue already have a task executor") + } + if ueScenario.Tasks == nil { + return errors.New("tasks list is nil") + } + if ueScenario.Config == (config.Ue{}) { + return errors.New("config is empty") + } + if s.reportChan == nil { + return errors.New("scenario manager's report channel is not set") + } + if s.gnbs == nil { + return errors.New("scenario manager's gnb list is not set") + } + ue := ueTasksCtx{ + done: false, + nextTasks: ueScenario.Tasks, + workerChan: make(chan Task, 1), + } + + worker := ueTaskExecutor{ + UeId: ueId, + UeCfg: ueScenario.Config, + TaskChan: ue.workerChan, + ReportChan: s.reportChan, + Gnbs: s.gnbs, + Wg: s.taskExecutorWg, + } + worker.Run() + + s.ues[ueId] = &ue + return nil +} + func (s *ScenarioManager) executeScenarios() { for !s.stop { select { @@ -106,7 +176,7 @@ func (s *ScenarioManager) executeScenarios() { case report := <-s.reportChan: s.handleReport(report) if err := s.manageNextTask(report.ueId, s.ueScenarios[report.ueId-1]); err != nil { - if !s.ueScenarios[report.ueId-1].Hang { + if !s.ueScenarios[report.ueId-1].Persist { log.Debugf("Stopping simulation for ue %d, reason: %s", report.ueId, err) s.ues[report.ueId].done = true s.stop = true @@ -121,28 +191,6 @@ func (s *ScenarioManager) executeScenarios() { } } -func (s *ScenarioManager) cleanup() { - drainAndCloseOrderRateController(s.registrationQueue) - - go func() { - open := true - var report report - for open { - report, open = <-s.reportChan - log.Debug("Got report from UE ", report.ueId, " during cleanup: ", report.reason) - } - }() - - for _, ue := range s.ues { - ue.workerChan <- Task{ - TaskType: Terminate, - } - } - - s.taskExecuterWg.Wait() - close(s.reportChan) -} - func (s *ScenarioManager) handleReport(report report) { if !report.success { s.ues[report.ueId].nextTasks = []Task{} @@ -194,75 +242,26 @@ func (s *ScenarioManager) restartUeScenario(ueId int, ueScenario UEScenario) err return nil } -func (s *ScenarioManager) setupGnbs(gnbConfs []config.GNodeB, amfConfs []*config.AMF) { - s.gnbs = make(map[string]*context.GNBContext) - - for gnbConf := range gnbConfs { - s.gnbs[gnbConfs[gnbConf].PlmnList.GnbId] = InitGnb(gnbConfs[gnbConf], amfConfs, s.gnbWg) - s.gnbWg.Add(1) - } - - // Wait for gNB to be connected before registering UEs - // TODO: We should wait for NGSetupResponse instead - time.Sleep(2 * time.Second) -} - -func (s *ScenarioManager) setupUeTaskExecuter(ueId int, ueScenario UEScenario) error { - if ueId < 1 { - return errors.New("ueId must be at least 1") - } - _, exist := s.ues[ueId] - if exist { - return errors.New("this ue already have a task executer") - } - if ueScenario.Tasks == nil { - return errors.New("tasks list is nil") - } - if ueScenario.Config == (config.Ue{}) { - return errors.New("config is empty") - } - if s.reportChan == nil { - return errors.New("scenario manager's report channel is not set") - } - if s.gnbs == nil { - return errors.New("scenario manager's gnb list is not set") - } - ue := ueTasksCtx{ - done: false, - nextTasks: ueScenario.Tasks, - workerChan: make(chan Task, 1), - } - - worker := ueTaskExecuter{ - UeId: ueId, - UeCfg: ueScenario.Config, - TaskChan: ue.workerChan, - ReportChan: s.reportChan, - Gnbs: s.gnbs, - Wg: s.taskExecuterWg, - } - worker.Run() +func (s *ScenarioManager) cleanup() { + drainAndCloseOrderRateController(s.registrationQueue) - s.ues[ueId] = &ue - return nil -} + go func() { + open := true + var report report + for open { + report, open = <-s.reportChan + log.Debug("Got report from UE ", report.ueId, " during cleanup: ", report.reason) + } + }() -func startOrderRateController(orderChan <-chan order, timeBetweenRegistration int) { - waitTime := time.Duration(timeBetweenRegistration) * time.Millisecond - orderTicker := time.NewTicker(waitTime) - var order order - var open bool - for { - <-orderTicker.C - orderTicker.Stop() - order, open = <-orderChan - if open { - order.workerChan <- order.task - orderTicker.Reset(waitTime) - } else { - return + for _, ue := range s.ues { + ue.workerChan <- Task{ + TaskType: Terminate, } } + + s.taskExecutorWg.Wait() + close(s.reportChan) } func drainAndCloseOrderRateController(orderChan chan order) { diff --git a/internal/scenario/scenarioManager_test.go b/internal/scenario/scenarioManager_test.go index 1566e172..0845d8cc 100644 --- a/internal/scenario/scenarioManager_test.go +++ b/internal/scenario/scenarioManager_test.go @@ -33,7 +33,7 @@ func getDefaultManager() ScenarioManager { ue.workerChan = make(chan Task, 5) ue.nextTasks = []Task{} s.ues[defaultUeId] = ue - s.taskExecuterWg = &sync.WaitGroup{} + s.taskExecutorWg = &sync.WaitGroup{} s.gnbWg = &sync.WaitGroup{} return s } @@ -623,7 +623,7 @@ func TestSetupGnbs(t *testing.T) { } } -func TestSetupUeTaskExecuter(t *testing.T) { +func TestSetupUeTaskExecutor(t *testing.T) { type input struct { manager ScenarioManager ueScenario UEScenario @@ -714,8 +714,8 @@ func TestSetupUeTaskExecuter(t *testing.T) { s := scenario.input.manager s.reportChan = make(chan report, 1) s.gnbs = map[string]*context.GNBContext{scenario.input.gnbs.GetGnbId(): scenario.input.gnbs} - s.taskExecuterWg = &sync.WaitGroup{} - s.setupUeTaskExecuter(scenario.input.ueId, scenario.input.ueScenario) + s.taskExecutorWg = &sync.WaitGroup{} + s.setupUeTaskExecutor(scenario.input.ueId, scenario.input.ueScenario) time.Sleep(time.Duration(100) * time.Millisecond) @@ -733,7 +733,7 @@ func TestSetupUeTaskExecuter(t *testing.T) { } -func TestSetupUeTaskExecuterHasError(t *testing.T) { +func TestSetupUeTaskExecutorHasError(t *testing.T) { gnbConf := config.GNodeB{ ControlIF: config.ControlIF{ @@ -785,19 +785,19 @@ func TestSetupUeTaskExecuterHasError(t *testing.T) { s := getDefaultManager() s.reportChan = make(chan report, 1) s.gnbs = map[string]*context.GNBContext{gnb.GetGnbId(): gnb} - s.taskExecuterWg = &sync.WaitGroup{} + s.taskExecutorWg = &sync.WaitGroup{} ueid := 0 - assert.Error(t, s.setupUeTaskExecuter(ueid, ueScenario)) + assert.Error(t, s.setupUeTaskExecutor(ueid, ueScenario)) }) - t.Run("Task Executer already exists", func(t *testing.T) { + t.Run("Task Executor already exists", func(t *testing.T) { s := getDefaultManager() s.reportChan = make(chan report, 1) s.gnbs = map[string]*context.GNBContext{gnb.GetGnbId(): gnb} - s.taskExecuterWg = &sync.WaitGroup{} + s.taskExecutorWg = &sync.WaitGroup{} ueid := 0 - s.setupUeTaskExecuter(ueid, ueScenario) - assert.Error(t, s.setupUeTaskExecuter(ueid, ueScenario)) + s.setupUeTaskExecutor(ueid, ueScenario) + assert.Error(t, s.setupUeTaskExecutor(ueid, ueScenario)) }) } @@ -841,8 +841,8 @@ func TestStartOrderRateController(t *testing.T) { func TestCleanup(t *testing.T) { s := getDefaultManager() - s.taskExecuterWg.Add(1) - time.AfterFunc(time.Duration(1)*time.Second, func() { s.taskExecuterWg.Done() }) + s.taskExecutorWg.Add(1) + time.AfterFunc(time.Duration(1)*time.Second, func() { s.taskExecutorWg.Done() }) s.reportChan = make(chan report, 2) s.reportChan <- report{ueId: 1, reason: "test"} s.reportChan <- report{ueId: 2, reason: "test"} diff --git a/internal/scenario/ueTaskExecuter.go b/internal/scenario/ueTaskExecuter.go index 03c0a2da..4427b87a 100644 --- a/internal/scenario/ueTaskExecuter.go +++ b/internal/scenario/ueTaskExecuter.go @@ -23,7 +23,7 @@ var ( NewUe = ue.NewUE ) -type ueTaskExecuter struct { +type ueTaskExecutor struct { UeId int UeCfg config.Ue TaskChan chan Task @@ -44,7 +44,7 @@ type ueTaskExecuter struct { loop bool } -func (e *ueTaskExecuter) Run() { +func (e *ueTaskExecutor) Run() { if e.running { log.Errorf("simulation for ue %d failed: already running", e.UeId) return @@ -81,7 +81,7 @@ func (e *ueTaskExecuter) Run() { go e.listen() } -func (e *ueTaskExecuter) listen() { +func (e *ueTaskExecutor) listen() { e.loop = true log.Infof("simulation started for ue %d", e.UeId) e.sendReport(true, "simulation started") @@ -98,7 +98,7 @@ func (e *ueTaskExecuter) listen() { e.running = false } -func (e *ueTaskExecuter) handleUeMsg(ueMsg scenario.ScenarioMessage) { +func (e *ueTaskExecutor) handleUeMsg(ueMsg scenario.ScenarioMessage) { msg := fmt.Sprintf("Switched from state %d to state %d ", e.state, ueMsg.StateChange) if ueMsg.StateChange == e.targetState { e.sendReport(true, msg) @@ -106,7 +106,7 @@ func (e *ueTaskExecuter) handleUeMsg(ueMsg scenario.ScenarioMessage) { e.state = ueMsg.StateChange } -func (e *ueTaskExecuter) handleTaskTimer(task Task) { +func (e *ueTaskExecutor) handleTaskTimer(task Task) { if task.Delay > 0 { task.Delay = max(0, task.Delay-int(time.Since(e.lastReceivedTaskTime).Milliseconds())) e.timedTasks[task] = time.AfterFunc(time.Duration(task.Delay)*time.Millisecond, func() { @@ -121,7 +121,7 @@ func (e *ueTaskExecuter) handleTaskTimer(task Task) { e.lastReceivedTaskTime = time.Now() } -func (e *ueTaskExecuter) handleTask(task Task) { +func (e *ueTaskExecutor) handleTask(task Task) { log.Debugf("[UE-%d] Received Task: %s", e.UeId, task.TaskType.ToStr()) log.Debugf("UE %d is attached to %s, tasked to %s", e.UeId, e.attachedGnb, task.TaskType.ToStr()) @@ -133,7 +133,7 @@ func (e *ueTaskExecuter) handleTask(task Task) { e.dispatchTask(task) } -func (e *ueTaskExecuter) dispatchTask(task Task) { +func (e *ueTaskExecutor) dispatchTask(task Task) { switch task.TaskType { case AttachToGNB: e.handleAttachToGnb(task) @@ -160,7 +160,7 @@ func (e *ueTaskExecuter) dispatchTask(task Task) { } } -func (e *ueTaskExecuter) handleAttachToGnb(task Task) { +func (e *ueTaskExecutor) handleAttachToGnb(task Task) { if e.Gnbs[task.Parameters.GnbId] != nil { // Create a new UE coroutine // ue.NewUE returns context of the new UE @@ -178,28 +178,28 @@ func (e *ueTaskExecuter) handleAttachToGnb(task Task) { } } -func (e *ueTaskExecuter) handleRegistration() { +func (e *ueTaskExecutor) handleRegistration() { e.ueRx <- procedures.UeTesterMessage{Type: procedures.Registration} e.targetState = ueCtx.MM5G_REGISTERED } -func (e *ueTaskExecuter) handleDeregistration() { +func (e *ueTaskExecutor) handleDeregistration() { e.ueRx <- procedures.UeTesterMessage{Type: procedures.Deregistration} e.targetState = ueCtx.MM5G_DEREGISTERED } -func (e *ueTaskExecuter) handleIdle() { +func (e *ueTaskExecutor) handleIdle() { e.ueRx <- procedures.UeTesterMessage{Type: procedures.Idle} e.targetState = ueCtx.MM5G_IDLE // TODO Should sync with gnb before sending Idle success report } -func (e *ueTaskExecuter) handleServiceRequest() { +func (e *ueTaskExecutor) handleServiceRequest() { e.ueRx <- procedures.UeTesterMessage{Type: procedures.ServiceRequest} e.targetState = ueCtx.MM5G_REGISTERED } -func (e *ueTaskExecuter) handleNewPduSession(task Task) { +func (e *ueTaskExecutor) handleNewPduSession(task Task) { if e.state == ueCtx.MM5G_REGISTERED { e.ueRx <- procedures.UeTesterMessage{Type: procedures.NewPDUSession} // TODO: Implement way to check if pduSession is successful @@ -211,7 +211,7 @@ func (e *ueTaskExecuter) handleNewPduSession(task Task) { } } -func (e *ueTaskExecuter) handleXnHandover(task Task) { +func (e *ueTaskExecutor) handleXnHandover(task Task) { if e.Gnbs[task.Parameters.GnbId] != nil { trigger.TriggerXnHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) e.attachedGnb = task.Parameters.GnbId @@ -226,7 +226,7 @@ func (e *ueTaskExecuter) handleXnHandover(task Task) { } } -func (e *ueTaskExecuter) handleNgapHandover(task Task) { +func (e *ueTaskExecutor) handleNgapHandover(task Task) { if e.Gnbs[task.Parameters.GnbId] != nil { trigger.TriggerNgapHandover(e.Gnbs[e.attachedGnb], e.Gnbs[task.Parameters.GnbId], int64(e.UeId)) e.attachedGnb = task.Parameters.GnbId @@ -241,7 +241,7 @@ func (e *ueTaskExecuter) handleNgapHandover(task Task) { } } -func (e *ueTaskExecuter) handleTerminate() { +func (e *ueTaskExecutor) handleTerminate() { e.ueRx <- procedures.UeTesterMessage{Type: procedures.Terminate} open := true for open { @@ -255,13 +255,13 @@ func (e *ueTaskExecuter) handleTerminate() { e.Wg.Done() } -func (e *ueTaskExecuter) handleKill() { +func (e *ueTaskExecutor) handleKill() { e.ueRx <- procedures.UeTesterMessage{Type: procedures.Kill} e.reset() e.sendReport(true, "UE Killed") } -func (e *ueTaskExecuter) reset() { +func (e *ueTaskExecutor) reset() { e.lock.Lock() e.ueRx = nil e.ueTx = nil @@ -274,6 +274,6 @@ func (e *ueTaskExecuter) reset() { e.lock.Unlock() } -func (e *ueTaskExecuter) sendReport(success bool, msg string) { +func (e *ueTaskExecutor) sendReport(success bool, msg string) { e.ReportChan <- report{success: success, reason: msg, ueId: e.UeId} } diff --git a/internal/templates/test-multi-ues-in-queue.go b/internal/templates/test-multi-ues-in-queue.go index e3b251e8..a8ba6055 100644 --- a/internal/templates/test-multi-ues-in-queue.go +++ b/internal/templates/test-multi-ues-in-queue.go @@ -120,7 +120,7 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb ueScenario.Loop = loop if tasks[len(tasks)-1].TaskType != scenario.Deregistration { - ueScenario.Hang = true + ueScenario.Persist = true } ueScenarios = append(ueScenarios, ueScenario) @@ -131,7 +131,7 @@ func TestMultiUesInQueue(numUes int, tunnelMode config.TunnelMode, dedicatedGnb func nextGnbConf(gnb config.GNodeB, i int, baseId string) config.GNodeB { var err error - gnb.PlmnList.GnbId = genereateGnbId(i, baseId) + gnb.PlmnList.GnbId = generateGnbId(i, baseId) gnb.ControlIF.Ip, err = tools.IncrementIP(gnb.ControlIF.Ip, "0.0.0.0/0") if err != nil { log.Fatal("[GNB][CONFIG] Error while allocating ip for N2: " + err.Error()) @@ -143,7 +143,7 @@ func nextGnbConf(gnb config.GNodeB, i int, baseId string) config.GNodeB { return gnb } -func genereateGnbId(i int, gnbId string) string { +func generateGnbId(i int, gnbId string) string { gnbId_int, err := strconv.ParseInt(gnbId, 16, 0) if err != nil { @@ -151,6 +151,6 @@ func genereateGnbId(i int, gnbId string) string { } base := int(gnbId_int) + i - gnbId = fmt.Sprintf("%06x", base) + gnbId = fmt.Sprintf("%06X", base) return gnbId } diff --git a/test/pr_test.go b/test/pr_test.go index 15e20eb1..1caad10f 100644 --- a/test/pr_test.go +++ b/test/pr_test.go @@ -64,9 +64,9 @@ func TestSingleUe(t *testing.T) { gnbs := []config.GNodeB{conf.GNodeB} ueScenario := scenario.UEScenario{ - Config: conf.Ue, - Loop: false, - Hang: false, + Config: conf.Ue, + Loop: false, + Persist: false, Tasks: []scenario.Task{ { TaskType: scenario.AttachToGNB, @@ -147,9 +147,9 @@ func TestRegistrationToCtxReleaseWithPDUSession(t *testing.T) { tmpConf := conf tmpConf.Ue.Msin = tools.IncrementMsin(i, conf.Ue.Msin) ueScenario := scenario.UEScenario{ - Config: tmpConf.Ue, - Loop: false, - Hang: false, + Config: tmpConf.Ue, + Loop: false, + Persist: false, Tasks: []scenario.Task{ { TaskType: scenario.AttachToGNB, @@ -242,9 +242,9 @@ func TestUERegistrationLoop(t *testing.T) { gnbs := []config.GNodeB{conf.GNodeB} ueScenario := scenario.UEScenario{ - Config: conf.Ue, - Loop: true, - Hang: false, + Config: conf.Ue, + Loop: true, + Persist: false, Tasks: []scenario.Task{ { TaskType: scenario.AttachToGNB,