From 4f60810d32261688130901a32ea0293df80e9dfd Mon Sep 17 00:00:00 2001 From: Jaroslava Fiedlerova Date: Wed, 3 Apr 2024 17:17:37 +0200 Subject: [PATCH 1/5] Compute and return requested bitrate from Iperf_ComputeModifiedBW For iperf3 (version 3.9) UDP, sender bitrate is not always equal to requested bitrate (defined by '-b' option). We need to retrieve information about the requsted bitrate from iperf command line options specified for each UDP testcase. This parameter is then used in UDP analysis functions for proper evaluation of the test results. --- ci-scripts/cls_oaicitest.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/ci-scripts/cls_oaicitest.py b/ci-scripts/cls_oaicitest.py index 102eb1dfe7..6dc9a23c96 100644 --- a/ci-scripts/cls_oaicitest.py +++ b/ci-scripts/cls_oaicitest.py @@ -66,26 +66,28 @@ import numpy as np def Iperf_ComputeModifiedBW(idx, ue_num, profile, args): result = re.search('-b\s*(?P[0-9\.]+)(?P[KMG])', str(args)) if result is None: - raise Exception('Iperf bandwidth not found or in incorrect format!') - iperf_bandwidth = result.group('iperf_bandwidth') + raise ValueError(f'requested iperf bandwidth not found in iperf options "{args}"') + iperf_bandwidth = float(result.group('iperf_bandwidth')) + if iperf_bandwidth == 0: + raise ValueError('iperf bandwidth set to 0 - invalid value') if profile == 'balanced': - iperf_bandwidth_new = float(iperf_bandwidth)/ue_num - if profile == 'single-ue': - iperf_bandwidth_new = float(iperf_bandwidth) + iperf_bandwidth_new = iperf_bandwidth/ue_num + if profile =='single-ue': + iperf_bandwidth_new = iperf_bandwidth if profile == 'unbalanced': # residual is 2% of max bw - residualBW = float(iperf_bandwidth) / 50 + residualBW = iperf_bandwidth / 50 if idx == 0: - iperf_bandwidth_new = float(iperf_bandwidth) - ((ue_num - 1) * residualBW) + iperf_bandwidth_new = iperf_bandwidth - ((ue_num - 1) * residualBW) else: iperf_bandwidth_new = residualBW iperf_bandwidth_str = result.group(0) iperf_bandwidth_unit = result.group(2) iperf_bandwidth_str_new = f"-b {'%.2f' % iperf_bandwidth_new}{iperf_bandwidth_unit}" - result = re.sub(iperf_bandwidth_str, iperf_bandwidth_str_new, str(args)) - if result is None: - raise Exception('Calculate Iperf bandwidth failed!') - return result + args_new = re.sub(iperf_bandwidth_str, iperf_bandwidth_str_new, str(args)) + if iperf_bandwidth_unit == 'K': + iperf_bandwidth_new = iperf_bandwidth_new / 1000 + return iperf_bandwidth_new, args_new def Iperf_ComputeTime(args): result = re.search('-t\s*(?P\d+)', str(args)) @@ -801,7 +803,7 @@ class OaiCiTest(): logPath = f'../cmake_targets/log/{ymlPath[1]}' if udpIperf: - iperf_opt = Iperf_ComputeModifiedBW(idx, ue_num, self.iperf_profile, self.iperf_args) + target_bitrate, iperf_opt = Iperf_ComputeModifiedBW(idx, ue_num, self.iperf_profile, self.iperf_args) # note: for UDP testing we don't want to use json report - reports 0 Mbps received bitrate jsonReport = "" # note: enable server report collection on the UE side, no need to store and collect server report separately on the server side From ef5df38fd2599c703c4862fa909a812d43ba832b Mon Sep 17 00:00:00 2001 From: Jaroslava Fiedlerova Date: Wed, 3 Apr 2024 17:22:09 +0200 Subject: [PATCH 2/5] Use requested bitrate in iperf3 UDP analysis function Sender bitrate is not always equal to the requested bitrate (set by "-b" iperf option) in iperf3 (v3.9) UDP tests, so we have to compture bitrate performance of UDP iperf3 test as receiver over requested bitrate. Bitrate performance then express percentage of requested bitrate, which is received on the receiver side. --- ci-scripts/cls_oaicitest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ci-scripts/cls_oaicitest.py b/ci-scripts/cls_oaicitest.py index 6dc9a23c96..fe75faa04f 100644 --- a/ci-scripts/cls_oaicitest.py +++ b/ci-scripts/cls_oaicitest.py @@ -142,7 +142,7 @@ def Iperf_analyzeV3BIDIRJson(filename): msg += f'Receiver Bitrate UL : {receiver_bitrate_ul} Mbps\n' return (True, msg) -def Iperf_analyzeV3UDP(filename, iperf_bitrate_threshold, iperf_packetloss_threshold): +def Iperf_analyzeV3UDP(filename, iperf_bitrate_threshold, iperf_packetloss_threshold, target_bitrate): if (not os.path.isfile(filename)): return (False, 'Iperf3 UDP: Log file not present') if (os.path.getsize(filename)==0): @@ -173,15 +173,15 @@ def Iperf_analyzeV3UDP(filename, iperf_bitrate_threshold, iperf_packetloss_thres sender_bitrate = float(sender_bitrate) / 1000 if receiver_unit == 'Kbits/sec': receiver_bitrate = float(receiver_bitrate) / 1000 - br_perf = 100 * float(receiver_bitrate) / float(sender_bitrate) + br_perf = 100 * float(receiver_bitrate) / float(target_bitrate) br_perf = '%.2f ' % br_perf sender_bitrate = '%.2f ' % float(sender_bitrate) receiver_bitrate = '%.2f ' % float(receiver_bitrate) - req_msg = f'Sender Bitrate : {sender_bitrate} Mbps' - bir_msg = f'Receiver Bitrate : {receiver_bitrate} Mbps' + req_msg = f'Sender Bitrate : {sender_bitrate} Mbps' + bir_msg = f'Receiver Bitrate: {receiver_bitrate} Mbps' brl_msg = f'{br_perf}%' - jit_msg = f'Jitter : {receiver_jitter}' - pal_msg = f'Packet Loss : {receiver_packetloss} %' + jit_msg = f'Jitter : {receiver_jitter}' + pal_msg = f'Packet Loss : {receiver_packetloss} %' if float(br_perf) < float(iperf_bitrate_threshold): brl_msg = f'too low! < {iperf_bitrate_threshold}%' if float(receiver_packetloss) > float(iperf_packetloss_threshold): @@ -837,7 +837,7 @@ class OaiCiTest(): else: cmd_ue.copyin(f'/tmp/{client_filename}', client_filename) if udpIperf: - status, msg = Iperf_analyzeV3UDP(client_filename, self.iperf_bitrate_threshold, self.iperf_packetloss_threshold) + status, msg = Iperf_analyzeV3UDP(client_filename, self.iperf_bitrate_threshold, self.iperf_packetloss_threshold, target_bitrate) elif bidirIperf: status, msg = Iperf_analyzeV3BIDIRJson(client_filename) else: From 8e6f7d9594cb8d032d53cb11f8f528303200a599 Mon Sep 17 00:00:00 2001 From: Jaroslava Fiedlerova Date: Wed, 3 Apr 2024 17:29:16 +0200 Subject: [PATCH 3/5] Use requested bitrate in iperf2 UDP analysis function Simplification of the code by replacing iperf_opt by target_bitrate (returned from Iperf_ComputeModifiedBW) - no need to repeat the steps to retrieve information about requested bitrate from iperf_opt. Compute iperf bitrate performance as receiver bitrate over requested (target) bitrate. Fix an issue with misdetection of missing report line - "result" was defined 2x in the function. With this MR, result can be only None or stores output of re.search() for matching the report line pattern, which ensure proper detection of the report line as well as presence off all required components in the report line. --- ci-scripts/cls_oaicitest.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/ci-scripts/cls_oaicitest.py b/ci-scripts/cls_oaicitest.py index fe75faa04f..0facb2f51a 100644 --- a/ci-scripts/cls_oaicitest.py +++ b/ci-scripts/cls_oaicitest.py @@ -191,29 +191,21 @@ def Iperf_analyzeV3UDP(filename, iperf_bitrate_threshold, iperf_packetloss_thres else: return (False, 'Could not analyze iperf report') -def Iperf_analyzeV2UDP(server_filename, iperf_bitrate_threshold, iperf_packetloss_threshold, iperf_opt): +def Iperf_analyzeV2UDP(server_filename, iperf_bitrate_threshold, iperf_packetloss_threshold, target_bitrate): + result = None if (not os.path.isfile(server_filename)): - return (False, 'Could not analyze, server report not found!') + return (False, 'Iperf UDP: Server report not found!') + if (os.path.getsize(server_filename)==0): + return (False, 'Iperf UDP: Log file is empty') # Computing the requested bandwidth in float - req_bw = 1.0 # default iperf throughput, in Mbps - result = re.search('-b *(?P[0-9\.]+)(?P[kKMG])', iperf_opt) - if result is not None: - req_bw = float(result.group('iperf_bandwidth')) - magn = result.group('magnitude') - if magn == "k" or magn == "K": - req_bw /= 1000 - elif magn == "G": - req_bw *= 1000 - statusTemplate = '(?:|\[ *\d+\].*) +0\.0-\s*(?P[0-9\.]+) +sec +[0-9\.]+ [kKMG]Bytes +(?P[0-9\.]+) (?P[kKMG])bits\/sec +(?P[0-9\.]+) ms +(\d+\/ *\d+) +(\((?P[0-9\.]+)%\))' - + statusTemplate = r'(?:|\[ *\d+\].*) +0\.0-\s*(?P[0-9\.]+) +sec +[0-9\.]+ [kKMG]Bytes +(?P[0-9\.]+) (?P[kKMG])bits\/sec +(?P[0-9\.]+) ms +(\d+\/ *\d+) +(\((?P[0-9\.]+)%\))' with open(server_filename, 'r') as server_file: for line in server_file.readlines(): - res = re.search(statusTemplate, str(line)) - if res is not None: - result = res + result = re.search(statusTemplate, str(line)) + if result is not None: + break if result is None: return (False, 'Could not parse server report!') - bitrate = float(result.group('bitrate')) magn = result.group('magnitude') if magn == "k" or magn == "K": @@ -222,16 +214,16 @@ def Iperf_analyzeV2UDP(server_filename, iperf_bitrate_threshold, iperf_packetlos bitrate *= 1000 jitter = float(result.group('jitter')) packetloss = float(result.group('packetloss')) - br_perf = float(bitrate)/float(req_bw) * 100 + br_perf = float(bitrate)/float(target_bitrate) * 100 br_perf = '%.2f ' % br_perf result = float(br_perf) >= float(iperf_bitrate_threshold) and float(packetloss) <= float(iperf_packetloss_threshold) - req_msg = f'Req Bitrate : {req_bw}' - bir_msg = f'Bitrate : {bitrate}' + req_msg = f'Req Bitrate : {target_bitrate}' + bir_msg = f'Bitrate : {bitrate}' brl_msg = f'Bitrate Perf: {br_perf} %' if float(br_perf) < float(iperf_bitrate_threshold): brl_msg += f' (too low! <{iperf_bitrate_threshold}%)' - jit_msg = f'Jitter : {jitter}' + jit_msg = f'Jitter : {jitter}' pal_msg = f'Packet Loss : {packetloss}' if float(packetloss) > float(iperf_packetloss_threshold): pal_msg += f' (too high! >{self.iperf_packetloss_threshold}%)' @@ -821,7 +813,7 @@ class OaiCiTest(): cmd_svr.run(f'{svr.getCmdPrefix()} iperf -c {ueIP} -B {svrIP} {iperf_opt} -i1 2>&1 | tee {client_filename}', timeout=iperf_time*1.5) cmd_ue.run(f'cp {client_filename} {logPath}/{client_filename}') cmd_ue.run(f'cp {server_filename} {logPath}/{server_filename}') - status, msg = Iperf_analyzeV2UDP(server_filename, self.iperf_bitrate_threshold, self.iperf_packetloss_threshold, iperf_opt) + status, msg = Iperf_analyzeV2UDP(server_filename, self.iperf_bitrate_threshold, self.iperf_packetloss_threshold, target_bitrate) else: with cls_cmd.getConnection(ue.getHost()) as cmd_ue, cls_cmd.getConnection(EPC.IPAddress) as cmd_svr: port = 5002 + idx From 7732689b9b56310fccc2d9795053c7de382e2599 Mon Sep 17 00:00:00 2001 From: Jaroslava Fiedlerova Date: Fri, 5 Apr 2024 16:12:11 +0200 Subject: [PATCH 4/5] Remove unused awk scripts --- ci-scripts/active_net_interfaces.awk | 21 ------------ ci-scripts/add_user_to_conf_file.awk | 33 ------------------- ci-scripts/add_user_to_subscriber_list.awk | 27 --------------- ci-scripts/main.py | 4 --- ci-scripts/ran.py | 1 - ci-scripts/tcp_iperf_stats.awk | 38 ---------------------- 6 files changed, 124 deletions(-) delete mode 100644 ci-scripts/active_net_interfaces.awk delete mode 100644 ci-scripts/add_user_to_conf_file.awk delete mode 100644 ci-scripts/add_user_to_subscriber_list.awk delete mode 100644 ci-scripts/tcp_iperf_stats.awk diff --git a/ci-scripts/active_net_interfaces.awk b/ci-scripts/active_net_interfaces.awk deleted file mode 100644 index 6d1174c1cc..0000000000 --- a/ci-scripts/active_net_interfaces.awk +++ /dev/null @@ -1,21 +0,0 @@ -#/* -# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more -# * contributor license agreements. See the NOTICE file distributed with -# * this work for additional information regarding copyright ownership. -# * The OpenAirInterface Software Alliance licenses this file to You under -# * the OAI Public License, Version 1.1 (the "License"); you may not use this file -# * except in compliance with the License. -# * You may obtain a copy of the License at -# * -# * http://www.openairinterface.org/?page_id=698 -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, -# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# * See the License for the specific language governing permissions and -# * limitations under the License. -# *------------------------------------------------------------------------------- -# * For more information about the OpenAirInterface (OAI) Software Alliance: -# * contact@openairinterface.org -# */ -/inet.*brd/{print "interfaceToUse="$NF"done"} diff --git a/ci-scripts/add_user_to_conf_file.awk b/ci-scripts/add_user_to_conf_file.awk deleted file mode 100644 index b338d5e7ac..0000000000 --- a/ci-scripts/add_user_to_conf_file.awk +++ /dev/null @@ -1,33 +0,0 @@ -BEGIN{lineIdx=0;captureUEDesc=0} -{ - if ($0 ~/UE0/) { - captureUEDesc = 1 - } - if (captureUEDesc == 1) { - captureLine[lineIdx] = $0 - lineIdx = lineIdx + 1 - } - print $0 -} -END { - for (ueIdx = 1; ueIdx < num_ues; ueIdx++) { - print "" - for (k = 0; k < lineIdx; k++) { - if (captureLine[k] ~/UE0/) { - mLine = captureLine[k] - gsub("UE0", "UE"ueIdx, mLine) - print mLine - } else { - if (captureLine[k] ~/MSIN=/) { - mLine = captureLine[k] - MSIN=sprintf("%08d", 1111+int(ueIdx)) - gsub("00001111", MSIN, mLine) - print mLine - } else { - print captureLine[k] - } - } - - } - } -} diff --git a/ci-scripts/add_user_to_subscriber_list.awk b/ci-scripts/add_user_to_subscriber_list.awk deleted file mode 100644 index 1e25990f59..0000000000 --- a/ci-scripts/add_user_to_subscriber_list.awk +++ /dev/null @@ -1,27 +0,0 @@ -BEGIN{lineIdx=0} -{ - captureLine[lineIdx] = $0 - lineIdx = lineIdx + 1 - print $0 -} -END{ - for (ueIdx = 1; ueIdx < num_ues; ueIdx++) { - for (k = 0; k < lineIdx; k++) { - if (captureLine[k] ~/UserName=/) { - mLine = captureLine[k] - MSIN=sprintf("%08d", 1111+int(ueIdx)) - gsub("00001111", MSIN, mLine) - print mLine - } else { - if (captureLine[k] ~/SubscriptionIndex/) { - mLine = captureLine[k] - MSIN=sprintf("%d", 111+int(ueIdx)) - gsub("111", MSIN, mLine) - print mLine - } else { - print captureLine[k] - } - } - } - } -} diff --git a/ci-scripts/main.py b/ci-scripts/main.py index ad93646de9..2387032e37 100644 --- a/ci-scripts/main.py +++ b/ci-scripts/main.py @@ -611,10 +611,6 @@ elif re.match('^TesteNB$', mode, re.IGNORECASE) or re.match('^TestUE$', mode, re if RAN.eNBIPAddress == '' or RAN.eNBUserName == '' or RAN.eNBPassword == '' or RAN.eNBSourceCodePath == '': HELP.eNBSrvHelp(RAN.eNBIPAddress, RAN.eNBUserName, RAN.eNBPassword, RAN.eNBSourceCodePath) sys.exit('Insufficient Parameter') - - if (EPC.IPAddress!= '') and (EPC.IPAddress != 'none'): - SSH.copyout(EPC.IPAddress, EPC.UserName, EPC.Password, cwd + "/tcp_iperf_stats.awk", "/tmp") - SSH.copyout(EPC.IPAddress, EPC.UserName, EPC.Password, cwd + "/active_net_interfaces.awk", "/tmp") else: if CiTestObj.UEIPAddress == '' or CiTestObj.ranRepository == '' or CiTestObj.ranBranch == '' or CiTestObj.UEUserName == '' or CiTestObj.UEPassword == '' or CiTestObj.UESourceCodePath == '': HELP.GenericHelp(CONST.Version) diff --git a/ci-scripts/ran.py b/ci-scripts/ran.py index 52b9c79abf..afa6036c7e 100644 --- a/ci-scripts/ran.py +++ b/ci-scripts/ran.py @@ -366,7 +366,6 @@ class RANManagement(): self.testCase_id = HTML.testCase_id mySSH = SSH.SSHConnection() cwd = os.getcwd() - mySSH.copyout(lIpAddr,lUserName,lPassWord, cwd + "/active_net_interfaces.awk", "/tmp") #Get pcap on enb and/or gnb if enabled in the xml if self.eNB_Trace=='yes': diff --git a/ci-scripts/tcp_iperf_stats.awk b/ci-scripts/tcp_iperf_stats.awk deleted file mode 100644 index b21bf16a2f..0000000000 --- a/ci-scripts/tcp_iperf_stats.awk +++ /dev/null @@ -1,38 +0,0 @@ -#/* -# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more -# * contributor license agreements. See the NOTICE file distributed with -# * this work for additional information regarding copyright ownership. -# * The OpenAirInterface Software Alliance licenses this file to You under -# * the OAI Public License, Version 1.1 (the "License"); you may not use this file -# * except in compliance with the License. -# * You may obtain a copy of the License at -# * -# * http://www.openairinterface.org/?page_id=698 -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, -# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# * See the License for the specific language governing permissions and -# * limitations under the License. -# *------------------------------------------------------------------------------- -# * For more information about the OpenAirInterface (OAI) Software Alliance: -# * contact@openairinterface.org -# */ -BEGIN{max=0;min=10000} -{ - if ($0 ~/Mbits/) { - if ($0 ~/KBytes/) { - split($0,a,"KBytes") - } else { - split($0,a,"MBytes") - } - split(a[2],b) - if (b[1]>max) { - max=b[1] - } - if (b[1] Date: Mon, 8 Apr 2024 16:18:42 +0200 Subject: [PATCH 5/5] Put Launch/StopPySharkCapture function out of the class --- ci-scripts/cls_containerize.py | 50 ++++++++++++++++------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/ci-scripts/cls_containerize.py b/ci-scripts/cls_containerize.py index 2eb0a98d16..c5e99500b1 100644 --- a/ci-scripts/cls_containerize.py +++ b/ci-scripts/cls_containerize.py @@ -169,6 +169,27 @@ def AnalyzeBuildLogs(buildRoot, images, globalStatus): collectInfo[image] = files return collectInfo +# pyshark livecapture launches 2 processes: +# * One using dumpcap -i lIfs -w - (ie redirecting the packets to STDOUT) +# * One using tshark -i - -w loFile (ie capturing from STDIN from previous process) +# but in fact the packets are read by the following loop before being in fact +# really written to loFile. +# So it is mandatory to keep the loop +def LaunchPySharkCapture(lIfs, lFilter, loFile): + capture = pyshark.LiveCapture(interface=lIfs, bpf_filter=lFilter, output_file=loFile, debug=False) + for packet in capture.sniff_continuously(): + pass + +def StopPySharkCapture(testcase): + with cls_cmd.LocalCmd() as myCmd: + cmd = 'killall tshark' + myCmd.run(cmd, reportNonZero=False) + cmd = 'killall dumpcap' + myCmd.run(cmd, reportNonZero=False) + time.sleep(5) + cmd = f'mv /tmp/capture_{testcase}.pcap ../cmake_targets/log/{testcase}/.' + myCmd.run(cmd, timeout=100, reportNonZero=False) + return False #----------------------------------------------------------- # Class Declaration #----------------------------------------------------------- @@ -1183,17 +1204,6 @@ class Containerize(): self.UndeployGenObject(HTML, RAN, UE) self.exitStatus = 1 - # pyshark livecapture launches 2 processes: - # * One using dumpcap -i lIfs -w - (ie redirecting the packets to STDOUT) - # * One using tshark -i - -w loFile (ie capturing from STDIN from previous process) - # but in fact the packets are read by the following loop before being in fact - # really written to loFile. - # So it is mandatory to keep the loop - def LaunchPySharkCapture(self, lIfs, lFilter, loFile): - capture = pyshark.LiveCapture(interface=lIfs, bpf_filter=lFilter, output_file=loFile, debug=False) - for packet in capture.sniff_continuously(): - pass - def CaptureOnDockerNetworks(self): myCmd = cls_cmd.LocalCmd(d = self.yamlPath[0]) cmd = 'docker-compose -f docker-compose-ci.yml config | grep com.docker.network.bridge.name | sed -e "s@^.*name: @@"' @@ -1218,7 +1228,7 @@ class Containerize(): myCmd.run(cmd, timeout=5, reportNonZero=False) myCmd.close() return - x = threading.Thread(target = self.LaunchPySharkCapture, args = (interfaces,capture_filter,output_file,)) + x = threading.Thread(target = LaunchPySharkCapture, args = (interfaces,capture_filter,output_file,)) x.daemon = True x.start() @@ -1307,21 +1317,9 @@ class Containerize(): HTML.CreateHtmlTestRow('UE log Analysis', 'KO', logStatus) else: HTML.CreateHtmlTestRow('UE log Analysis', 'OK', CONST.ALL_PROCESSES_OK) - - if self.tsharkStarted: - self.tsharkStarted = True - cmd = 'killall tshark' - myCmd2.run(cmd, reportNonZero=False) - cmd = 'killall dumpcap' - myCmd2.run(cmd, reportNonZero=False) - time.sleep(5) - ymlPath = self.yamlPath[0].split('/') - # The working dir is still logPath - cmd = f'mv /tmp/capture_{ymlPath[1]}.pcap .' - myCmd2.run(cmd, timeout=100, reportNonZero=False) - self.tsharkStarted = False myCmd2.close() - + if self.tsharkStarted: + self.tsharkStarted = StopPySharkCapture(ymlPath[1]) logging.debug('\u001B[1m Undeploying \u001B[0m') logging.debug(f'Working dir is back {self.yamlPath[0]}') cmd = 'docker-compose -f docker-compose-ci.yml down -v'