mirror of
https://gitlab.eurecom.fr/oai/openairinterface5g.git
synced 2026-07-13 04:30:28 +00:00
CI: Cleanup of parameters, change of branch and image naming, remove BuildProxy MR Summary: 1. Remove merge step from Jenkinsfile-GitLab-Container Removes the manual git merge step that was previously performed inside the `RAN-Container-Parent` pipeline before running CI. The `doGitLabMerge.sh` call and its merge-conflict failure handler are dropped from the init stage. The `RAN-Local-Repo-Push` stage no longer wraps triggerSlaveJob in catchError, instead, its failure posts an MR comment directly on GitLab to report the issue, and abort the follow-up test stages. 2. Switch to full commit IDs for image and branch naming Replaces abbreviated 8-character commit hashes with full commit IDs everywhere they are used to construct Docker image tags and CI branch names in internal git repository. 3. Remove L2sim proxy build support For the `RAN-L2-Sim-Test-4G`, the CI now assumes that the required proxy image is already present on the testing machine. Build proxy step is no longer needed, since there haven't been any recent updates in the proxy repo, and the proxyCommit we currently use is outdated anyway. 4. Rename and clean up parameters in Python scripts Renames legacy ran* / eNB* prefixed attributes to generic names across all CI Python classes. Also removes duplicate and unused legacy CLI argument aliases (--eNBRepository, --eNBBranch, --eNBIPAddress, etc.) from args_parse.py. |Old name |New name | |----------------|----------------| |ranRepository |repository | |ranBranch |branch | |ranCommitID |commitID | |ranAllowMerge |merge | |ranTargetBranch |targetBranch | |eNBSourceCodePath|workspace | Reviewed-by: Robert Schmidt <robert.schmidt@openairinterface.org>
609 lines
27 KiB
Python
609 lines
27 KiB
Python
# SPDX-License-Identifier: LicenseRef-CSSL-1.0
|
|
|
|
#---------------------------------------------------------------------
|
|
#
|
|
#
|
|
# Required Python Version
|
|
# Python 3.x
|
|
#
|
|
# Required Python Package
|
|
# pexpect
|
|
#---------------------------------------------------------------------
|
|
|
|
|
|
#-----------------------------------------------------------
|
|
# Import Libs
|
|
#-----------------------------------------------------------
|
|
import re # reg
|
|
import time # sleep
|
|
import os
|
|
import logging
|
|
import concurrent.futures
|
|
import json
|
|
|
|
#import our libs
|
|
import constants as CONST
|
|
|
|
import cls_module
|
|
import cls_corenetwork
|
|
import cls_analysis
|
|
import cls_cmd
|
|
from cls_ci_helper import archiveArtifact
|
|
|
|
#-----------------------------------------------------------
|
|
# Helper functions used here and in other classes
|
|
#-----------------------------------------------------------
|
|
def Iperf_ComputeModifiedBW(idx, ue_num, profile, args):
|
|
result = re.search(r'-b\s*(?P<iperf_bandwidth>[0-9\.]+)(?P<unit>[KMG])', str(args))
|
|
if result is None:
|
|
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 = iperf_bandwidth/ue_num
|
|
if profile =='single-ue':
|
|
iperf_bandwidth_new = iperf_bandwidth
|
|
if profile == 'unbalanced':
|
|
# residual is 2% of max bw
|
|
residualBW = iperf_bandwidth / 50
|
|
if idx == 0:
|
|
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}"
|
|
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(r'-t\s*(?P<iperf_time>\d+)', str(args))
|
|
if result is None:
|
|
raise Exception('Iperf time not found!')
|
|
return int(result.group('iperf_time'))
|
|
|
|
def Iperf_UpdateBindPort(opts, ueIP, idx):
|
|
# search if bind address present. Extract if yes, add if no.
|
|
bind_m = re.search(r'(-B|--bind)\s+(?P<ip>\d+\.\d+\.\d+\.\d+)', opts)
|
|
if bind_m:
|
|
bindIP = bind_m.group('ip')
|
|
else:
|
|
bindIP = ueIP
|
|
opts += f" -B {ueIP}"
|
|
# search if port present. Extract if yes, add if no.
|
|
port_m = re.search(r'(-p|--port)\s+(?P<port>\d+)', opts)
|
|
if port_m:
|
|
port = port_m.group('port')
|
|
else:
|
|
port = 5002 + idx
|
|
opts += f" -p {port}"
|
|
return bindIP, port, opts
|
|
|
|
def convert_to_mbps(value, magnitude):
|
|
value = float(value)
|
|
if magnitude == 'K' or magnitude == 'k':
|
|
return value / 1000
|
|
elif magnitude == 'M':
|
|
return value
|
|
elif magnitude == 'G':
|
|
return value * 1000
|
|
else:
|
|
return value
|
|
|
|
def extract_iperf_data(res):
|
|
if not res:
|
|
return None
|
|
bitrate_val = res.group('bitrate')
|
|
magnitude = res.group('magnitude')
|
|
return {
|
|
'bitrate_mbps': convert_to_mbps(bitrate_val, magnitude),
|
|
'jitter': res.group('jitter'),
|
|
'packetloss': res.group('packetloss'),
|
|
'bitrate_disp': f'{float(bitrate_val):.2f} {magnitude}bps'
|
|
}
|
|
|
|
def Iperf_analyzeV3TCPJson(filename, iperf_tcp_rate_target):
|
|
try:
|
|
with open(filename) as f:
|
|
results = json.load(f)
|
|
sender_bitrate = round(results['end']['streams'][0]['sender']['bits_per_second'] / 1000000, 2)
|
|
receiver_bitrate = round(results['end']['streams'][0]['receiver']['bits_per_second'] / 1000000, 2)
|
|
except json.JSONDecodeError as e:
|
|
return (False, f'Could not decode JSON log file {filename}: {e}')
|
|
except KeyError as e:
|
|
e_msg = results.get('error', f'error report not found in {filename}')
|
|
return (False, f'While parsing Iperf3 results: missing key {e}, {e_msg}')
|
|
except Exception as e:
|
|
return (False, f'While parsing Iperf3 results: exception: {e}')
|
|
snd_msg = f'Sender Bitrate : {sender_bitrate} Mbps'
|
|
rcv_msg = f'Receiver Bitrate : {receiver_bitrate} Mbps'
|
|
success = True
|
|
if iperf_tcp_rate_target is not None:
|
|
success = float(receiver_bitrate) >= float(iperf_tcp_rate_target)
|
|
if success:
|
|
rcv_msg += f" (target: {iperf_tcp_rate_target})"
|
|
else:
|
|
rcv_msg += f" (too low! < {iperf_tcp_rate_target})"
|
|
return(success, f'{snd_msg}\n{rcv_msg}')
|
|
|
|
def Iperf_analyzeV3BIDIRJson(filename):
|
|
try:
|
|
with open(filename) as f:
|
|
results = json.load(f)
|
|
sender_bitrate_ul = round(results['end']['streams'][0]['sender']['bits_per_second'] / 1000000, 2)
|
|
receiver_bitrate_ul = round(results['end']['streams'][0]['receiver']['bits_per_second'] / 1000000, 2)
|
|
sender_bitrate_dl = round(results['end']['streams'][1]['sender']['bits_per_second'] / 1000000, 2)
|
|
receiver_bitrate_dl = round(results['end']['streams'][1]['receiver']['bits_per_second'] / 1000000, 2)
|
|
except json.JSONDecodeError as e:
|
|
return (False, f'Could not decode JSON log file: {e}')
|
|
except KeyError as e:
|
|
e_msg = results.get('error', f'error report not found in {filename}')
|
|
return (False, f'While parsing Iperf3 results: missing key {e}, {e_msg}')
|
|
except Exception as e:
|
|
return (False, f'While parsing Iperf3 results: exception: {e}')
|
|
|
|
msg = f'Sender Bitrate DL : {sender_bitrate_dl} Mbps\n'
|
|
msg += f'Receiver Bitrate DL : {receiver_bitrate_dl} Mbps\n'
|
|
msg += f'Sender Bitrate UL : {sender_bitrate_ul} Mbps\n'
|
|
msg += f'Receiver Bitrate UL : {receiver_bitrate_ul} Mbps\n'
|
|
return (True, msg)
|
|
|
|
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:
|
|
return (False, 'Iperf3 UDP: Log file is empty')
|
|
|
|
sender_data = None
|
|
receiver_data = None
|
|
with open(filename, 'r') as server_file:
|
|
for line in server_file:
|
|
res_sender = re.search(r'(?P<bitrate>[0-9\.]+)\s+(?P<magnitude>[kKMG]?)bits\/sec\s+(?P<jitter>[0-9\.]+\s+ms)\s+(?P<lostPack>-?\d+)/(?P<sentPack>-?\d+)\s+\((?P<packetloss>[0-9\.eE\-\+]+).*?\s+(sender)', line)
|
|
res_receiver = re.search(r'(?P<bitrate>[0-9\.]+)\s+(?P<magnitude>[kKMG]?)bits\/sec\s+(?P<jitter>[0-9\.]+\s+ms)\s+(?P<lostPack>-?\d+)/(?P<receivedPack>-?\d+)\s+\((?P<packetloss>[0-9\.eE\-\+]+)%\).*?(receiver)', line)
|
|
if res_sender:
|
|
sender_data = extract_iperf_data(res_sender)
|
|
if res_receiver:
|
|
receiver_data = extract_iperf_data(res_receiver)
|
|
if not sender_data or not receiver_data:
|
|
return (False, 'Could not analyze iperf report')
|
|
|
|
br_perf = 100 * receiver_data['bitrate_mbps'] / float(target_bitrate)
|
|
br_perf_str = f'{br_perf:.2f}%'
|
|
req_msg = f"Sender Bitrate : {sender_data['bitrate_disp']}"
|
|
bir_msg = f"Receiver Bitrate: {receiver_data['bitrate_disp']} ({br_perf_str})"
|
|
jit_msg = f"Jitter : {receiver_data['jitter']}"
|
|
pal_msg = f"Packet Loss : {receiver_data['packetloss']}%"
|
|
if br_perf < float(iperf_bitrate_threshold):
|
|
bir_msg += f' (too low! < {iperf_bitrate_threshold}%)'
|
|
if float(receiver_data['packetloss']) > float(iperf_packetloss_threshold):
|
|
pal_msg += f' (too high! > {iperf_packetloss_threshold}%)'
|
|
result = br_perf >= float(iperf_bitrate_threshold) and float(receiver_data['packetloss']) <= float(iperf_packetloss_threshold)
|
|
return (result, f'{req_msg}\n{bir_msg}\n{jit_msg}\n{pal_msg}')
|
|
|
|
def Iperf_analyzeV2UDP(server_filename, iperf_bitrate_threshold, iperf_packetloss_threshold, target_bitrate):
|
|
result = None
|
|
if (not os.path.isfile(server_filename)):
|
|
return (False, 'Iperf UDP: Server report not found!')
|
|
if (os.path.getsize(server_filename)==0):
|
|
return (False, 'Iperf UDP: Log file is empty')
|
|
statusTemplate = r'(?:|\[ *\d+\].*) +0\.0+-\s*(?P<duration>[0-9\.]+) +sec +[0-9\.]+ [kKMG]Bytes +(?P<bitrate>[0-9\.]+) (?P<magnitude>[kKMG])bits\/sec +(?P<jitter>[0-9\.]+) ms +(\d+\/ *\d+) +(\((?P<packetloss>[0-9\.]+)%\))'
|
|
with open(server_filename, 'r') as server_file:
|
|
for line in server_file.readlines():
|
|
result = re.search(statusTemplate, str(line)) or result
|
|
if result is None:
|
|
return (False, 'Could not parse server report!')
|
|
bitrate_val = float(result.group('bitrate'))
|
|
magnitude = result.group('magnitude')
|
|
bitrate = convert_to_mbps(bitrate_val, magnitude)
|
|
jitter = float(result.group('jitter'))
|
|
packetloss = float(result.group('packetloss'))
|
|
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 : {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}'
|
|
pal_msg = f'Packet Loss : {packetloss}'
|
|
if float(packetloss) > float(iperf_packetloss_threshold):
|
|
pal_msg += f' (too high! >{iperf_packetloss_threshold}%)'
|
|
return (result, f'{req_msg}\n{bir_msg}\n{brl_msg}\n{jit_msg}\n{pal_msg}')
|
|
|
|
def Custom_Command(HTML, node, command):
|
|
logging.info(f"Executing custom command on {node}")
|
|
cmd = cls_cmd.getConnection(node)
|
|
ret = cmd.run(command)
|
|
cmd.close()
|
|
logging.debug(f"Custom_Command: {command} on node: {node} - {'OK, command succeeded' if ret.returncode == 0 else f'Error, return code: {ret.returncode}'}")
|
|
status = 'OK'
|
|
message = []
|
|
if ret.returncode != 0:
|
|
message = [ret.stdout]
|
|
logging.error(f'Custom_Command failed: output: {message}')
|
|
status = 'KO'
|
|
HTML.CreateHtmlTestRowQueue(command, status, message)
|
|
return status == 'OK' or status == 'Warning'
|
|
|
|
def Custom_Script(HTML, node, script, args):
|
|
logging.info(f"Executing custom script on {node}")
|
|
with cls_cmd.getConnection(node) as c:
|
|
ret = c.exec_script(script, 90, args)
|
|
logging.debug(f"Custom_Script: {script} on node: {node} - return code {ret.returncode}, output:\n{ret.stdout}")
|
|
status = 'OK'
|
|
message = [ret.stdout]
|
|
if ret.returncode != 0:
|
|
status = 'KO'
|
|
HTML.CreateHtmlTestRowQueue(script, status, message)
|
|
return status == 'OK' or status == 'Warning'
|
|
|
|
def IdleSleep(HTML, idle_sleep_time):
|
|
logging.debug(f"sleep for {idle_sleep_time} seconds")
|
|
time.sleep(idle_sleep_time)
|
|
HTML.CreateHtmlTestRow(f"{idle_sleep_time} sec", 'OK', CONST.ALL_PROCESSES_OK)
|
|
return True
|
|
|
|
def Deploy_Physim(ctx, HTML, node, workdir, script, options):
|
|
logging.debug(f'Running physims on server {node} workdir {workdir}')
|
|
with cls_cmd.getConnection(node) as c:
|
|
sys_info = c.exec_script("scripts/sys-info.sh", 5)
|
|
ret = c.exec_script(script, 1500, options)
|
|
logging.debug(f'"{script}" finished with code {ret.returncode}, output:\n{ret.stdout}')
|
|
HTML.CreateHtmlTestRowQueue('Query system info', 'OK', [sys_info.stdout])
|
|
with cls_cmd.getConnection(node) as ssh:
|
|
details_json = archiveArtifact(ssh, ctx, f'{workdir}/desc-tests.json')
|
|
result_junit = archiveArtifact(ssh, ctx, f'{workdir}/results-run.xml')
|
|
archiveArtifact(ssh, ctx, f'{workdir}/physim_log.txt')
|
|
archiveArtifact(ssh, ctx, f'{workdir}/LastTestsFailed.log')
|
|
archiveArtifact(ssh, ctx, f'{workdir}/LastTest.log')
|
|
test_status, test_summary, test_result = cls_analysis.Analysis.analyze_physim(result_junit, details_json, ctx.logPath)
|
|
if test_summary:
|
|
if test_status:
|
|
HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
|
|
HTML.CreateHtmlTestRowPhySimTestResult(test_summary, test_result)
|
|
logging.info('\u001B[1m Physical Simulator Pass\u001B[0m')
|
|
else:
|
|
HTML.CreateHtmlTestRowQueue('At least one physical simulator test failed!', 'KO', ["See below for details"])
|
|
HTML.CreateHtmlTestRowPhySimTestResult(test_summary, test_result)
|
|
logging.error('\u001B[1m Physical Simulator Fail\u001B[0m')
|
|
else:
|
|
HTML.CreateHtmlTestRowQueue('Physical simulator failed', 'KO', [test_result])
|
|
logging.error('\u001B[1m Physical Simulator Fail\u001B[0m')
|
|
return test_status
|
|
|
|
#-----------------------------------------------------------
|
|
# OaiCiTest Class Definition
|
|
#-----------------------------------------------------------
|
|
class OaiCiTest():
|
|
|
|
def __init__(self):
|
|
self.repository = ''
|
|
self.branch = ''
|
|
self.testXMLfiles = []
|
|
self.ping_args = ''
|
|
self.ping_packetloss_threshold = ''
|
|
self.ping_rttavg_threshold =''
|
|
self.iperf_args = ''
|
|
self.iperf_packetloss_threshold = ''
|
|
self.iperf_bitrate_threshold = ''
|
|
self.iperf_profile = ''
|
|
self.iperf_tcp_rate_target = ''
|
|
self.finalStatus = False
|
|
self.ue_ids = []
|
|
self.svr_node = None
|
|
self.svr_id = None
|
|
self.cmd_prefix = '' # prefix before {lte,nr}-uesoftmodem
|
|
|
|
def InitializeUE(self, node, HTML):
|
|
ues = [cls_module.Module_UE(n.strip(), node) for n in self.ue_ids]
|
|
messages = []
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.initialize) for ue in ues]
|
|
for f, ue in zip(futures, ues):
|
|
uename = f'UE {ue.getName()}'
|
|
messages.append(f'{uename}: initialized' if f.result() else f'{uename}: ERROR during Initialization')
|
|
[f.result() for f in futures]
|
|
HTML.CreateHtmlTestRowQueue('N/A', 'OK', messages)
|
|
return True
|
|
|
|
def AttachUE(self, node, HTML):
|
|
ues = [cls_module.Module_UE(ue_id, node) for ue_id in self.ue_ids]
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.attach) for ue in ues]
|
|
attached = [f.result() for f in futures]
|
|
futures = [executor.submit(ue.checkMTU) for ue in ues]
|
|
mtus = [f.result() for f in futures]
|
|
messages = [f"UE {ue.getName()}: {ue.getIP()}" for ue in ues]
|
|
success = all(attached) and all(mtus)
|
|
if success:
|
|
HTML.CreateHtmlTestRowQueue('N/A', 'OK', messages)
|
|
else:
|
|
logging.error(f'error attaching or wrong MTU: attached {attached}, mtus {mtus}')
|
|
HTML.CreateHtmlTestRowQueue('N/A', 'KO', ["Could not retrieve UE IP address(es) or MTU(s) wrong!"])
|
|
return success
|
|
|
|
def DetachUE(self, node, HTML):
|
|
ues = [cls_module.Module_UE(ue_id, node) for ue_id in self.ue_ids]
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.detach) for ue in ues]
|
|
[f.result() for f in futures]
|
|
messages = [f"UE {ue.getName()}: detached" for ue in ues]
|
|
HTML.CreateHtmlTestRowQueue('NA', 'OK', messages)
|
|
return True
|
|
|
|
def DataDisableUE(self, node, HTML):
|
|
ues = [cls_module.Module_UE(n.strip(), node) for n in self.ue_ids]
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.dataDisable) for ue in ues]
|
|
status = [f.result() for f in futures]
|
|
success = all(status)
|
|
if success:
|
|
messages = [f"UE {ue.getName()}: data disabled" for ue in ues]
|
|
HTML.CreateHtmlTestRowQueue('NA', 'OK', messages)
|
|
else:
|
|
logging.error(f'error enabling data: {status}')
|
|
HTML.CreateHtmlTestRowQueue('N/A', 'KO', ["Could not disable UE data!"])
|
|
return success
|
|
|
|
def DataEnableUE(self, node, HTML):
|
|
ues = [cls_module.Module_UE(n.strip(), node) for n in self.ue_ids]
|
|
logging.debug(f'disabling data for UEs {ues}')
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.dataEnable) for ue in ues]
|
|
status = [f.result() for f in futures]
|
|
success = all(status)
|
|
if success:
|
|
messages = [f"UE {ue.getName()}: data enabled" for ue in ues]
|
|
HTML.CreateHtmlTestRowQueue('NA', 'OK', messages)
|
|
else:
|
|
logging.error(f'error enabling data: {status}')
|
|
HTML.CreateHtmlTestRowQueue('N/A', 'KO', ["Could not enable UE data!"])
|
|
return success
|
|
|
|
def CheckStatusUE(self, node, HTML):
|
|
ues = [cls_module.Module_UE(n.strip(), node) for n in self.ue_ids]
|
|
logging.debug(f'checking status of UEs {ues}')
|
|
messages = []
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.check) for ue in ues]
|
|
messages = [f.result() for f in futures]
|
|
HTML.CreateHtmlTestRowQueue('NA', 'OK', messages)
|
|
return True
|
|
|
|
def Ping_common(self, ctx, cn, ue):
|
|
ping_status = 0
|
|
ueIP = ue.getIP()
|
|
if not ueIP:
|
|
return (False, f"UE {ue.getName()} has no IP address")
|
|
svrIP = cn.getIP()
|
|
if not svrIP:
|
|
return (False, f"CN {cn.getName()} has no IP address")
|
|
ping_log_file = f'/tmp/ping_{ue.getName()}.log'
|
|
ping_time = re.findall(r"-c *(\d+)",str(self.ping_args))
|
|
if re.search('%cn_ip%', self.ping_args) or re.search(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+', self.ping_args):
|
|
raise Exception(f"ping_args should not have IP address: {self.ping_args}")
|
|
interface = f'-I {ue.getIFName()}' if ue.getIFName() else ''
|
|
ping_cmd = f'{ue.getCmdPrefix()} ping {interface} {self.ping_args} {svrIP} 2>&1 | tee {ping_log_file}'
|
|
cmd = cls_cmd.getConnection(ue.getHost())
|
|
response = cmd.run(ping_cmd, timeout=int(ping_time[0])*1.5)
|
|
ue_header = f'UE {ue.getName()} ({ueIP})'
|
|
if response.returncode != 0:
|
|
message = ue_header + ': ping crashed: TIMEOUT?'
|
|
return (False, message)
|
|
|
|
local_ping_log_file = archiveArtifact(cmd, ctx, ping_log_file)
|
|
cmd.close()
|
|
|
|
with open(local_ping_log_file, 'r') as f:
|
|
ping_output = "".join(f.readlines())
|
|
result = re.search(r', (?P<packetloss>[0-9\.]+)% packet loss, time [0-9\.]+ms', ping_output)
|
|
if result is None:
|
|
message = ue_header + ': Packet Loss Not Found!'
|
|
return (False, message)
|
|
packetloss = result.group('packetloss')
|
|
result = re.search(r'rtt min\/avg\/max\/mdev = (?P<rtt_min>[0-9\.]+)\/(?P<rtt_avg>[0-9\.]+)\/(?P<rtt_max>[0-9\.]+)\/[0-9\.]+ ms', ping_output)
|
|
if result is None:
|
|
message = ue_header + ': Ping RTT_Min RTT_Avg RTT_Max Not Found!'
|
|
return (False, message)
|
|
rtt_min = result.group('rtt_min')
|
|
rtt_avg = result.group('rtt_avg')
|
|
rtt_max = result.group('rtt_max')
|
|
|
|
pal_msg = f'Packet Loss: {packetloss}%'
|
|
min_msg = f'RTT(Min) : {rtt_min} ms'
|
|
avg_msg = f'RTT(Avg) : {rtt_avg} ms'
|
|
max_msg = f'RTT(Max) : {rtt_max} ms'
|
|
|
|
message = f'{ue_header}\n{pal_msg}\n{min_msg}\n{avg_msg}\n{max_msg}'
|
|
|
|
#checking packet loss compliance
|
|
if float(packetloss) > float(self.ping_packetloss_threshold):
|
|
message += '\nPacket Loss too high'
|
|
return (False, message)
|
|
elif float(packetloss) > 0:
|
|
message += '\nPacket Loss is not 0%'
|
|
|
|
if self.ping_rttavg_threshold != '':
|
|
if float(rtt_avg) > float(self.ping_rttavg_threshold):
|
|
ping_rttavg_error_msg = f'RTT(Avg) too high: {rtt_avg} ms; Target: {self.ping_rttavg_threshold} ms'
|
|
message += f'\n {ping_rttavg_error_msg}'
|
|
return (False, message)
|
|
|
|
return (True, message)
|
|
|
|
def Ping(self, ctx, node, HTML, infra_file="ci_infra.yaml"):
|
|
if self.ue_ids == [] or self.svr_id == None:
|
|
raise Exception("no module names in self.ue_ids or/and self.svr_id provided")
|
|
ues = [cls_module.Module_UE(ue_id, node, infra_file) for ue_id in self.ue_ids]
|
|
cn = cls_corenetwork.CoreNetwork(self.svr_id, self.svr_node, filename=infra_file)
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(self.Ping_common, ctx, cn, ue) for ue in ues]
|
|
results = [f.result() for f in futures]
|
|
# each result in results is a tuple, first member goes to successes, second to messages
|
|
successes, messages = map(list, zip(*results))
|
|
|
|
success = len(successes) == len(ues) and all(successes)
|
|
logger = logging.info if success else logging.error
|
|
hcolor = "\u001B[1;37;44m" if success else "\u001B[1;37;41m"
|
|
lcolor = "\u001B[1;34m" if success else "\u001B[1;31m"
|
|
for m in messages:
|
|
lines = m.split('\n')
|
|
logger(f'{hcolor} ping result for {lines[0]} \u001B[0m')
|
|
for l in lines[1:]:
|
|
logger(f'{lcolor} {l}\u001B[0m')
|
|
|
|
if success:
|
|
HTML.CreateHtmlTestRowQueue(self.ping_args, 'OK', messages)
|
|
else:
|
|
HTML.CreateHtmlTestRowQueue(self.ping_args, 'KO', messages)
|
|
return success
|
|
|
|
def Iperf_Module(self, ctx, cn, ue, idx, ue_num):
|
|
ueIP = ue.getIP()
|
|
if not ueIP:
|
|
return (False, f"UE {ue.getName()} has no IP address")
|
|
svrIP = cn.getIP()
|
|
if not svrIP:
|
|
return (False, f"Iperf server {cn.getName()} has no IP address")
|
|
iperf_opt = self.iperf_args
|
|
jsonReport = "--json"
|
|
serverReport = ""
|
|
udpIperf = re.search('-u', iperf_opt) is not None
|
|
bidirIperf = re.search('--bidir', iperf_opt) is not None
|
|
client_filename = f'/tmp/iperf_client_{ue.getName()}.log'
|
|
if udpIperf:
|
|
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
|
|
serverReport = "--get-server-output"
|
|
iperf_time = Iperf_ComputeTime(self.iperf_args)
|
|
bindIP, port, iperf_opt = Iperf_UpdateBindPort(iperf_opt, ueIP, idx)
|
|
# hack: the ADB UEs don't have iperf in $PATH, so we need to hardcode for the moment
|
|
iperf_ue = '/data/local/tmp/iperf3' if re.search('adb', ue.getName()) else 'iperf3'
|
|
ue_header = f'UE {ue.getName()} ({bindIP})'
|
|
with cls_cmd.getConnection(ue.getHost()) as cmd_ue, cls_cmd.getConnection(cn.getHost()) as cmd_svr:
|
|
# note: some core setups start an iperf3 server automatically, indicated in ci_infra by runIperf3Server: False`
|
|
t = iperf_time * 2.5
|
|
cmd_ue.run(f'rm {client_filename}', reportNonZero=False, silent=True)
|
|
if cn.runIperf3Server():
|
|
# Clean up any existing iperf3 server processes on this port.
|
|
ret = cmd_svr.run(f"{cn.getCmdPrefix()} pkill -f '.*iperf3.*{port}'", reportNonZero=False)
|
|
# If pkill succeeds, it means there was a leftover iperf3 server.
|
|
if ret.returncode == 0:
|
|
logging.warning(f'Iperf3 server on port {port} detected and terminated')
|
|
cmd_svr.run(f'{cn.getCmdPrefix()} timeout -vk3 {t} iperf3 -s -B {svrIP} -p {port} -1 {jsonReport} >> /dev/null &', timeout=t)
|
|
client_ret = cmd_ue.run(f'{ue.getCmdPrefix()} timeout -vk3 {t} {iperf_ue} -c {svrIP} {iperf_opt} {jsonReport} {serverReport} -O 5 >> {client_filename}', timeout=t, reportNonZero=False)
|
|
dest_filename = archiveArtifact(cmd_ue, ctx, client_filename)
|
|
if udpIperf:
|
|
status, msg = Iperf_analyzeV3UDP(dest_filename, self.iperf_bitrate_threshold, self.iperf_packetloss_threshold, target_bitrate)
|
|
elif bidirIperf:
|
|
status, msg = Iperf_analyzeV3BIDIRJson(dest_filename)
|
|
else:
|
|
status, msg = Iperf_analyzeV3TCPJson(dest_filename, self.iperf_tcp_rate_target)
|
|
|
|
# add some diagnostic messages if the actual iperf command returned non-zero
|
|
if client_ret.returncode != 0:
|
|
msg = f'{msg}\nIperf client command failed on {bindIP} -> {svrIP}:{port} (return code: {client_ret.returncode})'
|
|
|
|
return (status, f'{ue_header}\n{msg}')
|
|
|
|
def Iperf(self, ctx, node, HTML, infra_file="ci_infra.yaml"):
|
|
logging.debug(f'Iperf: iperf_args "{self.iperf_args}" iperf_packetloss_threshold "{self.iperf_packetloss_threshold}" iperf_bitrate_threshold "{self.iperf_bitrate_threshold}" iperf_profile "{self.iperf_profile}"')
|
|
|
|
if self.ue_ids == [] or self.svr_id == None:
|
|
raise Exception("no module names in self.ue_ids or/and self.svr_id provided")
|
|
ues = [cls_module.Module_UE(ue_id, node, infra_file) for ue_id in self.ue_ids]
|
|
cn = cls_corenetwork.CoreNetwork(self.svr_id, self.svr_node, filename=infra_file)
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(self.Iperf_Module, ctx, cn, ue, i, len(ues)) for i, ue in enumerate(ues)]
|
|
results = [f.result() for f in futures]
|
|
# each result in results is a tuple, first member goes to successes, second to messages
|
|
successes, messages = map(list, zip(*results))
|
|
|
|
success = len(successes) == len(ues) and all(successes)
|
|
logger = logging.info if success else logging.error
|
|
hcolor = "\u001B[1;37;45m" if success else "\u001B[1;37;41m"
|
|
lcolor = "\u001B[1;35m" if success else "\u001B[1;31m"
|
|
for m in messages:
|
|
lines = m.split('\n')
|
|
logger(f'{hcolor} iperf result for {lines[0]} \u001B[0m')
|
|
for l in lines[1:]:
|
|
logger(f'{lcolor} {l}\u001B[0m')
|
|
|
|
if success:
|
|
HTML.CreateHtmlTestRowQueue(self.iperf_args, 'OK', messages)
|
|
else:
|
|
HTML.CreateHtmlTestRowQueue(self.iperf_args, 'KO', messages)
|
|
return success
|
|
|
|
def Iperf2_Unidir(self, ctx, node, HTML, infra_file="ci_infra.yaml"):
|
|
if self.ue_ids == [] or self.svr_id == None or len(self.ue_ids) != 1:
|
|
raise Exception("no module names in self.ue_ids or/and self.svr_id provided, multi UE scenario not supported")
|
|
ue = cls_module.Module_UE(self.ue_ids[0].strip(), node, infra_file)
|
|
cn = cls_corenetwork.CoreNetwork(self.svr_id, self.svr_node, filename=infra_file)
|
|
ueIP = ue.getIP()
|
|
if not ueIP:
|
|
return False
|
|
svrIP = cn.getIP()
|
|
if not svrIP:
|
|
return False
|
|
server_filename = f'/tmp/iperf_server_{ue.getName()}.log'
|
|
iperf_time = Iperf_ComputeTime(self.iperf_args)
|
|
target_bitrate, iperf_opt = Iperf_ComputeModifiedBW(0, 1, self.iperf_profile, self.iperf_args)
|
|
t = iperf_time*2.5
|
|
with cls_cmd.getConnection(ue.getHost()) as cmd_ue, cls_cmd.getConnection(cn.getHost()) as cmd_svr:
|
|
cmd_ue.run(f'rm {server_filename}', reportNonZero=False)
|
|
cmd_ue.run(f'{ue.getCmdPrefix()} timeout -vk3 {t} iperf -B {ueIP} -s -u -i1 >> {server_filename} &', timeout=t)
|
|
cmd_svr.run(f'{cn.getCmdPrefix()} timeout -vk3 {t} iperf -c {ueIP} -B {svrIP} {iperf_opt} -i1 >> /dev/null', timeout=t)
|
|
localPath = f'{os.getcwd()}'
|
|
local = archiveArtifact(cmd_ue, ctx, server_filename)
|
|
success, msg = Iperf_analyzeV2UDP(local, self.iperf_bitrate_threshold, self.iperf_packetloss_threshold, target_bitrate)
|
|
ue_header = f'UE {ue.getName()} ({ueIP})'
|
|
logging.info(f'\u001B[1;37;45m iperf result for {ue_header}\u001B[0m')
|
|
for l in msg.split('\n'):
|
|
logging.info(f'\u001B[1;35m {l} \u001B[0m')
|
|
if success:
|
|
HTML.CreateHtmlTestRowQueue(self.iperf_args, 'OK', [f'{ue_header}\n{msg}'])
|
|
else:
|
|
HTML.CreateHtmlTestRowQueue(self.iperf_args, 'KO', [f'{ue_header}\n{msg}'])
|
|
return success
|
|
|
|
def TerminateUE(self, ctx, node, HTML):
|
|
ues = [cls_module.Module_UE(n.strip(), node) for n in self.ue_ids]
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
|
|
futures = [executor.submit(ue.terminate, ctx) for ue in ues]
|
|
archives = [f.result() for f in futures]
|
|
archive_info = [f'Log at: {a}' if a else 'No log available' for a in archives]
|
|
messages = [f"UE {ue.getName()}: {log}" for (ue, log) in zip(ues, archive_info)]
|
|
HTML.CreateHtmlTestRowQueue(f'N/A', 'OK', messages)
|
|
return True
|
|
|
|
def DeployCoreNetwork(cn_id, ctx, HTML):
|
|
core_name = cn_id.strip()
|
|
cn = cls_corenetwork.CoreNetwork(core_name)
|
|
success, output = cn.deploy()
|
|
logging.info(f"deployment core network {core_name} success {success}, output:\n{output}")
|
|
if success:
|
|
msg = f"Started {cn} [{cn.getIP()}]"
|
|
HTML.CreateHtmlTestRowQueue(core_name, 'OK', [msg])
|
|
else:
|
|
msg = f"deployment of core network {core_name} FAILED"
|
|
logging.error(msg)
|
|
HTML.CreateHtmlTestRowQueue(core_name, 'KO', [msg])
|
|
return success
|
|
|
|
def UndeployCoreNetwork(cn_id, ctx, HTML):
|
|
core_name = cn_id.strip()
|
|
cn = cls_corenetwork.CoreNetwork(core_name)
|
|
logs, output = cn.undeploy(ctx=ctx)
|
|
logging.info(f"undeployed core network {core_name}, logs {logs}, output:\n{output}")
|
|
message = "Log files:\n" + "\n".join([os.path.basename(l) for l in logs])
|
|
HTML.CreateHtmlTestRowQueue(core_name, 'OK', [message])
|
|
return True
|