cls_containerize.py 63.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#/*
# * 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
# */
#---------------------------------------------------------------------
# Python for CI of OAI-eNB + COTS-UE
#
#   Required Python Version
#     Python 3.x
#
#   Required Python Package
#     pexpect
#---------------------------------------------------------------------

#-----------------------------------------------------------
# Import
#-----------------------------------------------------------
34 35
import sys	      # arg
import re	       # reg
36 37
import logging
import os
38
import shutil
39
import subprocess
40
import time
41 42
import pyshark
import threading
43
import cls_cmd
44
from multiprocessing import Process, Lock, SimpleQueue
45
from zipfile import ZipFile
46 47 48 49

#-----------------------------------------------------------
# OAI Testing modules
#-----------------------------------------------------------
50
import cls_cluster as OC
51
import cls_cmd
52 53 54
import sshconnection as SSH
import helpreadme as HELP
import constants as CONST
55
import cls_oaicitest
56

57 58 59 60
#-----------------------------------------------------------
# Helper functions used here and in other classes
# (e.g., cls_cluster.py)
#-----------------------------------------------------------
61 62
IMAGES = ['oai-enb', 'oai-lte-ru', 'oai-lte-ue', 'oai-gnb', 'oai-nr-cuup', 'oai-gnb-aw2s', 'oai-nr-ue']

63
def CreateWorkspace(sshSession, sourcePath, ranRepository, ranCommitID, ranTargetBranch, ranAllowMerge):
64 65 66 67 68
	if ranCommitID == '':
		logging.error('need ranCommitID in CreateWorkspace()')
		sys.exit('Insufficient Parameter in CreateWorkspace()')

	sshSession.command(f'rm -rf {sourcePath}', '\$', 10)
69 70
	sshSession.command('mkdir -p ' + sourcePath, '\$', 5)
	sshSession.command('cd ' + sourcePath, '\$', 5)
Robert Schmidt's avatar
Robert Schmidt committed
71
	# Recent version of git (>2.20?) should handle missing .git extension # without problems
Raphael Defosseux's avatar
Raphael Defosseux committed
72 73
	if ranTargetBranch == 'null':
		ranTargetBranch = 'develop'
74 75
	baseBranch = re.sub('origin/', '', ranTargetBranch)
	sshSession.command(f'git clone --filter=blob:none -n -b {baseBranch} {ranRepository} .', '\$', 60)
Robert Schmidt's avatar
Robert Schmidt committed
76 77
	if sshSession.getBefore().count('error') > 0 or sshSession.getBefore().count('error') > 0:
		sys.exit('error during clone')
78 79 80 81 82
	sshSession.command('git config user.email "jenkins@openairinterface.org"', '\$', 5)
	sshSession.command('git config user.name "OAI Jenkins"', '\$', 5)

	sshSession.command('mkdir -p cmake_targets/log', '\$', 5)
	# if the commit ID is provided use it to point to it
83 84 85 86 87 88
	sshSession.command(f'git checkout -f {ranCommitID}', '\$', 30)
	if sshSession.getBefore().count(f'HEAD is now at {ranCommitID[:6]}') != 1:
		sshSession.command('git log --oneline | head -n5', '\$', 5)
		logging.warning(f'problems during checkout, is at: {sshSession.getBefore()}')
	else:
		logging.debug('successful checkout')
89 90 91 92
	# if the branch is not develop, then it is a merge request and we need to do
	# the potential merge. Note that merge conflicts should already been checked earlier
	if ranAllowMerge:
		if ranTargetBranch == '':
93 94
			ranTargetBranch = 'develop'
		logging.debug(f'Merging with the target branch: {ranTargetBranch}')
95
		sshSession.command(f'git merge --ff origin/{ranTargetBranch} -m "Temporary merge for CI"', '\$', 30)
96

97 98 99
def ImageTagToUse(imageName, ranCommitID, ranBranch, ranAllowMerge):
	shortCommit = ranCommitID[0:8]
	if ranAllowMerge:
100 101 102
		# Allowing contributor to have a name/branchName format
		branchName = ranBranch.replace('/','-')
		tagToUse = f'{branchName}-{shortCommit}'
103 104 105 106 107
	else:
		tagToUse = f'develop-{shortCommit}'
	fullTag = f'{imageName}:{tagToUse}'
	return fullTag

108 109 110 111 112 113
def CopyLogsToExecutor(cmd, sourcePath, log_name):
	cmd.cd(f'{sourcePath}/cmake_targets')
	cmd.run(f'rm -f {log_name}.zip')
	cmd.run(f'mkdir -p {log_name}')
	cmd.run(f'mv log/* {log_name}')
	cmd.run(f'zip -r -qq {log_name}.zip {log_name}')
114 115 116 117 118 119

	# copy zip to executor for analysis
	if (os.path.isfile(f'./{log_name}.zip')):
		os.remove(f'./{log_name}.zip')
	if (os.path.isdir(f'./{log_name}')):
		shutil.rmtree(f'./{log_name}')
120 121
	cmd.copyin(f'{sourcePath}/cmake_targets/{log_name}.zip', f'./{log_name}.zip')
	cmd.run(f'rm -f {log_name}.zip')
122 123
	ZipFile(f'{log_name}.zip').extractall('.')

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
def AnalyzeBuildLogs(buildRoot, images, globalStatus):
	collectInfo = {}
	for image in images:
		files = {}
		file_list = [f for f in os.listdir(f'{buildRoot}/{image}') if os.path.isfile(os.path.join(f'{buildRoot}/{image}', f)) and f.endswith('.txt')]
		# Analyze the "sub-logs" of every target image
		for fil in file_list:
			errorandwarnings = {}
			warningsNo = 0
			errorsNo = 0
			with open(f'{buildRoot}/{image}/{fil}', mode='r') as inputfile:
				for line in inputfile:
					result = re.search(' ERROR ', str(line))
					if result is not None:
						errorsNo += 1
					result = re.search(' error:', str(line))
					if result is not None:
						errorsNo += 1
					result = re.search(' WARNING ', str(line))
					if result is not None:
						warningsNo += 1
					result = re.search(' warning:', str(line))
					if result is not None:
						warningsNo += 1
				errorandwarnings['errors'] = errorsNo
				errorandwarnings['warnings'] = warningsNo
				errorandwarnings['status'] = globalStatus
			files[fil] = errorandwarnings
		# Analyze the target image
		if os.path.isfile(f'{buildRoot}/{image}.log'):
			errorandwarnings = {}
			committed = False
			tagged = False
			with open(f'{buildRoot}/{image}.log', mode='r') as inputfile:
				for line in inputfile:
159 160 161 162 163 164
					lineHasTag = re.search(f'Successfully tagged {image}:', str(line)) is not None
					lineHasTag2 = re.search(f'naming to docker.io/library/{image}:', str(line)) is not None
					tagged = tagged or lineHasTag or lineHasTag2
					# the OpenShift Cluster builder prepends image registry URL
					lineHasCommit = re.search(f'COMMIT [a-zA-Z0-9\.:/\-]*{image}', str(line)) is not None
					committed = committed or lineHasCommit
165 166 167 168 169 170 171
			errorandwarnings['errors'] = 0 if committed or tagged else 1
			errorandwarnings['warnings'] = 0
			errorandwarnings['status'] = committed or tagged
			files['Target Image Creation'] = errorandwarnings
		collectInfo[image] = files
	return collectInfo

172 173
def AnalyzeIperf(cliOptions, clientReport, serverReport):
	req_bw = 1.0 # default iperf throughput, in Mbps
Robert Schmidt's avatar
Robert Schmidt committed
174
	result = re.search('-b *(?P<iperf_bandwidth>[0-9\.]+)(?P<magnitude>[kKMG])', cliOptions)
175 176 177
	if result is not None:
		req_bw = float(result.group('iperf_bandwidth'))
		magn = result.group('magnitude')
Robert Schmidt's avatar
Robert Schmidt committed
178
		if magn == "k" or magn == "K":
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
			req_bw /= 1000
		elif magn == "G":
			req_bw *= 1000
	req_dur = 10 # default iperf send duration
	result = re.search('-t *(?P<duration>[0-9]+)', cliOptions)
	if result is not None:
		req_dur = int(result.group('duration'))

	reportLine = None
	# find server report in client status
	clientReportLines = clientReport.split('\n')
	for l in range(len(clientReportLines)):
		res = re.search('read failed: Connection refused', clientReportLines[l])
		if res is not None:
			message = 'iperf connection refused by server!'
			logging.error(f'\u001B[1;37;41mIperf Test FAIL: {message}\u001B[0m')
			return (False, message)
		res = re.search('Server Report:', clientReportLines[l])
		if res is not None and l + 1 < len(clientReportLines):
			reportLine = clientReportLines[l+1]
			logging.debug(f'found server report: "{reportLine}"')

Robert Schmidt's avatar
Robert Schmidt committed
201
	statusTemplate = '(?:|\[ *\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\.]+)%\))'
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
	# if we do not find a server report in the client logs, check the server logs
	# and use the last line which is typically close/identical to server report
	if reportLine is None:
		for l in serverReport.split('\n'):
			res = re.search(statusTemplate, l)
			if res is not None:
				reportLine = l
		if reportLine is None:
			logging.warning('no report in server status found!')
			return (False, 'could not parse iperf logs')
		logging.debug(f'found client status: {reportLine}')

	result = re.search(statusTemplate, reportLine)
	if result is None:
		logging.error('could not analyze report from statusTemplate')
		return (False, 'could not parse iperf logs')

	duration = float(result.group('duration'))
	bitrate = float(result.group('bitrate'))
	magn = result.group('magnitude')
Robert Schmidt's avatar
Robert Schmidt committed
222
	if magn == "k" or magn == "K":
223
		bitrate /= 1000
Robert Schmidt's avatar
Robert Schmidt committed
224 225
	elif magn == "G": # we assume bitrate in Mbps, therefore it must be G now
		bitrate *= 1000
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
	jitter = float(result.group('jitter'))
	packetloss = float(result.group('packetloss'))

	logging.debug('\u001B[1;37;44m iperf result \u001B[0m')
	msg = f'Req Bitrate: {req_bw}'
	logging.debug(f'\u001B[1;34m{msg}\u001B[0m')

	br_loss = bitrate/req_bw
	bmsg = f'Bitrate    : {bitrate} (perf {br_loss})'
	logging.debug(f'\u001B[1;34m{bmsg}\u001B[0m')
	msg += '\n' + bmsg
	if br_loss < 0.9:
		msg += '\nBitrate performance too low (<90%)'
		logging.debug(f'\u001B[1;37;41mBitrate performance too low (<90%)\u001B[0m')
		return (False, msg)

	plmsg = f'Packet Loss: {packetloss}%'
	logging.debug(f'\u001B[1;34m{plmsg}\u001B[0m')
	msg += '\n' + plmsg
	if packetloss > 5.0:
		msg += '\nPacket Loss too high!'
		logging.debug(f'\u001B[1;37;41mPacket Loss too high \u001B[0m')
		return (False, msg)

	dmsg = f'Duration   : {duration} (req {req_dur})'
	logging.debug(f'\u001B[1;34m{dmsg}\u001B[0m')
	msg += '\n' + dmsg
	if duration < float(req_dur):
		msg += '\nDuration of iperf too short!'
		logging.debug(f'\u001B[1;37;41mDuration of iperf too short\u001B[0m')
		return (False, msg)

	jmsg = f'Jitter     : {jitter}'
	logging.debug(f'\u001B[1;34m{jmsg}\u001B[0m')
	msg += '\n' + jmsg
	return (True, msg)

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
#-----------------------------------------------------------
# Class Declaration
#-----------------------------------------------------------
class Containerize():

	def __init__(self):
		
		self.ranRepository = ''
		self.ranBranch = ''
		self.ranAllowMerge = False
		self.ranCommitID = ''
		self.ranTargetBranch = ''
		self.eNBIPAddress = ''
		self.eNBUserName = ''
		self.eNBPassword = ''
		self.eNBSourceCodePath = ''
		self.eNB1IPAddress = ''
		self.eNB1UserName = ''
		self.eNB1Password = ''
		self.eNB1SourceCodePath = ''
		self.eNB2IPAddress = ''
		self.eNB2UserName = ''
		self.eNB2Password = ''
		self.eNB2SourceCodePath = ''
		self.forcedWorkspaceCleanup = False
		self.imageKind = ''
289
		self.proxyCommit = None
290 291
		self.eNB_instance = 0
		self.eNB_serverId = ['', '', '']
292
		self.deployKind = [True, True, True]
293
		self.yamlPath = ['', '', '']
294 295 296
		self.services = ['', '', '']
		self.nb_healthy = [0, 0, 0]
		self.exitStatus = 0
297 298
		self.eNB_logFile = ['', '', '']

299
		self.testCase_id = ''
300

301
		self.cli = ''
302
		self.cliBuildOptions = ''
303 304
		self.dockerfileprefix = ''
		self.host = ''
305

306
		self.deployedContainers = []
307
		self.tsharkStarted = False
308
		self.displayedNewTags = False
309 310 311 312 313 314 315 316
		self.pingContName = ''
		self.pingOptions = ''
		self.pingLossThreshold = ''
		self.svrContName = ''
		self.svrOptions = ''
		self.cliContName = ''
		self.cliOptions = ''

317 318 319
		self.imageToCopy = ''
		self.registrySvrId = ''
		self.testSvrId = ''
320
		self.imageToPull = []
321 322 323
		#checkers from xml
		self.ran_checkers={}

324 325 326 327
#-----------------------------------------------------------
# Container management functions
#-----------------------------------------------------------

Raphael Defosseux's avatar
Raphael Defosseux committed
328
	def BuildImage(self, HTML):
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
		if self.ranRepository == '' or self.ranBranch == '' or self.ranCommitID == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
		if self.eNB_serverId[self.eNB_instance] == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
		logging.debug('Building on server: ' + lIpAddr)
351
		cmd = cls_cmd.RemoteCmd(lIpAddr)
352 353
	
		# Checking the hostname to get adapted on cli and dockerfileprefixes
354 355
		cmd.run('hostnamectl')
		result = re.search('Ubuntu|Red Hat', cmd.getBefore())
356 357 358
		self.host = result.group(0)
		if self.host == 'Ubuntu':
			self.cli = 'docker'
359
			self.dockerfileprefix = '.ubuntu20'
360
			self.cliBuildOptions = '--no-cache'
361
		elif self.host == 'Red Hat':
362
			self.cli = 'sudo podman'
363
			self.dockerfileprefix = '.rhel9'
364
			self.cliBuildOptions = '--no-cache --disable-compression'
365

366
		# we always build the ran-build image with all targets
367
		imageNames = [('ran-build', 'build')]
368
		result = re.search('eNB', self.imageKind)
369
		# Creating a tupple with the imageName and the DockerFile prefix pattern on obelix
370 371
		if result is not None:
			imageNames.append(('oai-enb', 'eNB'))
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
		result = re.search('gNB', self.imageKind)
		if result is not None:
			imageNames.append(('oai-gnb', 'gNB'))
		result = re.search('all', self.imageKind)
		if result is not None:
			imageNames.append(('oai-enb', 'eNB'))
			imageNames.append(('oai-gnb', 'gNB'))
			imageNames.append(('oai-nr-cuup', 'nr-cuup'))
			imageNames.append(('oai-lte-ue', 'lteUE'))
			imageNames.append(('oai-nr-ue', 'nrUE'))
			if self.host == 'Red Hat':
				imageNames.append(('oai-physim', 'phySim'))
			if self.host == 'Ubuntu':
				imageNames.append(('oai-lte-ru', 'lteRU'))
		result = re.search('build_cross_arm64', self.imageKind)
		if result is not None:
			self.dockerfileprefix = '.ubuntu20.cross-arm64'
389
		
390 391
		# Workaround for some servers, we need to erase completely the workspace
		if self.forcedWorkspaceCleanup:
392
			cmd.run(f'sudo -S rm -Rf {lSourcePath}')
393
	
Raphael Defosseux's avatar
Raphael Defosseux committed
394
		self.testCase_id = HTML.testCase_id
395
	
396
		CreateWorkspace(cmd, lSourcePath, self.ranRepository, self.ranCommitID, self.ranTargetBranch, self.ranAllowMerge)
397

398
		# if asterix, copy the entitlement and subscription manager configurations
399
		if self.host == 'Red Hat':
400 401 402 403
			cmd.run('mkdir -p ./etc-pki-entitlement ./rhsm-conf ./rhsm-ca')
			cmd.run('cp /etc/rhsm/rhsm.conf ./rhsm-conf/')
			cmd.run('cp /etc/rhsm/ca/redhat-uep.pem ./rhsm-ca/')
			cmd.run('cp /etc/pki/entitlement/*.pem ./etc-pki-entitlement/')
404

405 406 407
		baseImage = 'ran-base'
		baseTag = 'develop'
		forceBaseImageBuild = False
408
		imageTag = 'develop'
409 410
		if (self.ranAllowMerge):
			imageTag = 'ci-temp'
411
			if self.ranTargetBranch == 'develop':
412 413
				cmd.run(f'git diff HEAD..origin/develop -- cmake_targets/build_oai cmake_targets/tools/build_helper docker/Dockerfile.base{self.dockerfileprefix} | grep --colour=never -i INDEX')
				result = re.search('index', cmd.getBefore())
414
				if result is not None:
415 416
					forceBaseImageBuild = True
					baseTag = 'ci-temp'
417
		else:
418
			forceBaseImageBuild = True
419

420
		# Let's remove any previous run artifacts if still there
421
		cmd.run(f"{self.cli} image prune --force")
422
		if forceBaseImageBuild:
423
			cmd.run(f"{self.cli} image rm {baseImage}:{baseTag}")
424
		for image,pattern in imageNames:
425
			cmd.run(f"{self.cli} image rm {image}:{imageTag}")
426

427 428 429
		# Build the base image only on Push Events (not on Merge Requests)
		# On when the base image docker file is being modified.
		if forceBaseImageBuild:
430
			cmd.run(f"{self.cli} build {self.cliBuildOptions} --target {baseImage} --tag {baseImage}:{baseTag} --file docker/Dockerfile.base{self.dockerfileprefix} . &> cmake_targets/log/ran-base.log", timeout=1600)
431
		# First verify if the base image was properly created.
432
		ret = cmd.run(f"{self.cli} image inspect --format=\'Size = {{{{.Size}}}} bytes\' {baseImage}:{baseTag}")
433
		allImagesSize = {}
434
		if ret.returncode != 0:
435
			logging.error('\u001B[1m Could not build properly ran-base\u001B[0m')
436
			# Recover the name of the failed container?
437
			cmd.run(f"{self.cli} ps --quiet --filter \"status=exited\" -n1 | xargs --no-run-if-empty {self.cli} rm -f")
438 439
			cmd.run(f"{self.cli} image prune --force")
			cmd.close()
440 441 442 443
			logging.error('\u001B[1m Building OAI Images Failed\u001B[0m')
			HTML.CreateHtmlTestRow(self.imageKind, 'KO', CONST.ALL_PROCESSES_OK)
			HTML.CreateHtmlTabFooter(False)
			sys.exit(1)
444
		else:
445
			result = re.search('Size *= *(?P<size>[0-9\-]+) *bytes', cmd.getBefore())
446 447 448 449 450 451 452 453 454
			if result is not None:
				size = float(result.group("size")) / 1000000
				imageSizeStr = f'{size:.1f}'
				logging.debug(f'\u001B[1m   ran-base size is {imageSizeStr} Mbytes\u001B[0m')
				allImagesSize['ran-base'] = f'{imageSizeStr} Mbytes'
			else:
				logging.debug('ran-base size is unknown')

		# Recover build logs, for the moment only possible when build is successful
455 456 457 458
		cmd.run(f"{self.cli} create --name test {baseImage}:{baseTag}")
		cmd.run("mkdir -p cmake_targets/log/ran-base")
		cmd.run(f"{self.cli} cp test:/oai-ran/cmake_targets/log/. cmake_targets/log/ran-base")
		cmd.run(f"{self.cli} rm -f test")
459 460

		# Build the target image(s)
461 462
		status = True
		attemptedImages = ['ran-base']
463
		for image,pattern in imageNames:
464
			attemptedImages += [image]
465
			# the archived Dockerfiles have "ran-base:latest" as base image
466
			# we need to update them with proper tag
467
			cmd.run(f'sed -i -e "s#{baseImage}:latest#{baseImage}:{baseTag}#" docker/Dockerfile.{pattern}{self.dockerfileprefix}')
468
			if image != 'ran-build':
469
				cmd.run(f'sed -i -e "s#ran-build:latest#ran-build:{imageTag}#" docker/Dockerfile.{pattern}{self.dockerfileprefix}')
470 471
			ret = cmd.run(f'{self.cli} build {self.cliBuildOptions} --target {image} --tag {image}:{imageTag} --file docker/Dockerfile.{pattern}{self.dockerfileprefix} . > cmake_targets/log/{image}.log 2>&1', timeout=1200)
			if image == 'ran-build' and ret.returncode == 0:
472 473 474 475 476
				cmd.run(f"docker run --name test-log -d {image}:{imageTag} /bin/true")
				cmd.run(f"docker cp test-log:/oai-ran/cmake_targets/log/ cmake_targets/log/{image}/")
				cmd.run(f"docker rm -f test-log")
			else:
				cmd.run(f"mkdir -p cmake_targets/log/{image}")
477 478 479
			# check the status of the build
			ret = cmd.run(f"{self.cli} image inspect --format=\'Size = {{{{.Size}}}} bytes\' {image}:{imageTag}")
			if ret.returncode != 0:
480
				logging.error('\u001B[1m Could not build properly ' + image + '\u001B[0m')
481
				status = False
482
				# Here we should check if the last container corresponds to a failed command and destroy it
483
				cmd.run(f"{self.cli} ps --quiet --filter \"status=exited\" -n1 | xargs --no-run-if-empty {self.cli} rm -f")
484 485
				allImagesSize[image] = 'N/A -- Build Failed'
				break
486
			else:
487
				result = re.search('Size *= *(?P<size>[0-9\-]+) *bytes', cmd.getBefore())
488
				if result is not None:
489
					size = float(result.group("size")) / 1000000 # convert to MB
490 491 492
					imageSizeStr = f'{size:.1f}'
					logging.debug(f'\u001B[1m   {image} size is {imageSizeStr} Mbytes\u001B[0m')
					allImagesSize[image] = f'{imageSizeStr} Mbytes'
493
				else:
494 495
					logging.debug(f'{image} size is unknown')
					allImagesSize[image] = 'unknown'
496
			# Now pruning dangling images in between target builds
497
			cmd.run(f"{self.cli} image prune --force")
498

499
		# Remove all intermediate build images and clean up
500
		if self.ranAllowMerge and forceBaseImageBuild:
501 502 503
			cmd.run(f"{self.cli} image rm {baseImage}:{baseTag}")
		cmd.run(f"{self.cli} image rm ran-build:{imageTag}")
		cmd.run(f"{self.cli} volume prune --force")
504 505 506

		# create a zip with all logs
		build_log_name = f'build_log_{self.testCase_id}'
507 508
		CopyLogsToExecutor(cmd, lSourcePath, build_log_name)
		cmd.close()
509

510
		# Analyze the logs
511
		collectInfo = AnalyzeBuildLogs(build_log_name, attemptedImages, status)
512
		
513 514 515
		if status:
			logging.info('\u001B[1m Building OAI Image(s) Pass\u001B[0m')
			HTML.CreateHtmlTestRow(self.imageKind, 'OK', CONST.ALL_PROCESSES_OK)
516
			HTML.CreateHtmlNextTabHeaderTestRow(collectInfo, allImagesSize)
517 518 519
		else:
			logging.error('\u001B[1m Building OAI Images Failed\u001B[0m')
			HTML.CreateHtmlTestRow(self.imageKind, 'KO', CONST.ALL_PROCESSES_OK)
520
			HTML.CreateHtmlNextTabHeaderTestRow(collectInfo, allImagesSize)
521 522
			HTML.CreateHtmlTabFooter(False)
			sys.exit(1)
523

524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
	def BuildProxy(self, HTML):
		if self.ranRepository == '' or self.ranBranch == '' or self.ranCommitID == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
		if self.eNB_serverId[self.eNB_instance] == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
		if self.proxyCommit is None:
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter (need proxyCommit for proxy build)')
		logging.debug('Building on server: ' + lIpAddr)
		mySSH = SSH.SSHConnection()
		mySSH.open(lIpAddr, lUserName, lPassWord)

		# Check that we are on Ubuntu
		mySSH.command('hostnamectl', '\$', 5)
		result = re.search('Ubuntu',  mySSH.getBefore())
		self.host = result.group(0)
		if self.host != 'Ubuntu':
			logging.error('\u001B[1m Can build proxy only on Ubuntu server\u001B[0m')
			mySSH.close()
			sys.exit(1)

		self.cli = 'docker'
		self.cliBuildOptions = '--no-cache'

		# Workaround for some servers, we need to erase completely the workspace
		if self.forcedWorkspaceCleanup:
			mySSH.command('echo ' + lPassWord + ' | sudo -S rm -Rf ' + lSourcePath, '\$', 15)

		oldRanCommidID = self.ranCommitID
		oldRanRepository = self.ranRepository
		oldRanAllowMerge = self.ranAllowMerge
572
		oldRanTargetBranch = self.ranTargetBranch
573 574 575
		self.ranCommitID = self.proxyCommit
		self.ranRepository = 'https://github.com/EpiSci/oai-lte-5g-multi-ue-proxy.git'
		self.ranAllowMerge = False
576
		self.ranTargetBranch = 'master'
577
		CreateWorkspace(mySSH, lSourcePath, self.ranRepository, self.ranCommitID, self.ranTargetBranch, self.ranAllowMerge)
578 579 580 581
		# to prevent accidentally overwriting data that might be used later
		self.ranCommitID = oldRanCommidID
		self.ranRepository = oldRanRepository
		self.ranAllowMerge = oldRanAllowMerge
582
		self.ranTargetBranch = oldRanTargetBranch
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598

		# Let's remove any previous run artifacts if still there
		mySSH.command(self.cli + ' image prune --force', '\$', 30)
		# Remove any previous proxy image
		mySSH.command(self.cli + ' image rm oai-lte-multi-ue-proxy:latest || true', '\$', 30)

		tag = self.proxyCommit
		logging.debug('building L2sim proxy image for tag ' + tag)
		# check if the corresponding proxy image with tag exists. If not, build it
		mySSH.command(self.cli + ' image inspect --format=\'Size = {{.Size}} bytes\' proxy:' + tag, '\$', 5)
		buildProxy = mySSH.getBefore().count('o such image') != 0
		if buildProxy:
			mySSH.command(self.cli + ' build ' + self.cliBuildOptions + ' --target oai-lte-multi-ue-proxy --tag proxy:' + tag + ' --file docker/Dockerfile.ubuntu18.04 . > cmake_targets/log/proxy-build.log 2>&1', '\$', 180)
			# Note: at this point, OAI images are flattened, but we cannot do this
			# here, as the flatten script is not in the proxy repo
			mySSH.command(self.cli + ' image inspect --format=\'Size = {{.Size}} bytes\' proxy:' + tag, '\$', 5)
599
			mySSH.command(self.cli + ' image prune --force || true','\$', 15)
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
			if mySSH.getBefore().count('o such image') != 0:
				logging.error('\u001B[1m Build of L2sim proxy failed\u001B[0m')
				mySSH.close()
				HTML.CreateHtmlTestRow('commit ' + tag, 'KO', CONST.ALL_PROCESSES_OK)
				HTML.CreateHtmlTabFooter(False)
				sys.exit(1)
		else:
			logging.debug('L2sim proxy image for tag ' + tag + ' already exists, skipping build')

		# retag the build images to that we pick it up later
		mySSH.command('docker image tag proxy:' + tag + ' oai-lte-multi-ue-proxy:latest', '\$', 5)

		# no merge: is a push to develop, tag the image so we can push it to the registry
		if not self.ranAllowMerge:
			mySSH.command('docker image tag proxy:' + tag + ' proxy:develop', '\$', 5)

		# we assume that the host on which this is built will also run the proxy. The proxy
		# currently requires the following command, and the docker-compose up mechanism of
		# the CI does not allow to run arbitrary commands. Note that the following actually
		# belongs to the deployment, not the build of the proxy...
		logging.warning('the following command belongs to deployment, but no mechanism exists to exec it there!')
		mySSH.command('sudo ifconfig lo: 127.0.0.2 netmask 255.0.0.0 up', '\$', 5)

		# Analyzing the logs
		if buildProxy:
			self.testCase_id = HTML.testCase_id
			mySSH.command('cd ' + lSourcePath + '/cmake_targets', '\$', 5)
			mySSH.command('mkdir -p proxy_build_log_' + self.testCase_id, '\$', 5)
			mySSH.command('mv log/* ' + 'proxy_build_log_' + self.testCase_id, '\$', 5)
			if (os.path.isfile('./proxy_build_log_' + self.testCase_id + '.zip')):
				os.remove('./proxy_build_log_' + self.testCase_id + '.zip')
			if (os.path.isdir('./proxy_build_log_' + self.testCase_id)):
				shutil.rmtree('./proxy_build_log_' + self.testCase_id)
			mySSH.command('zip -r -qq proxy_build_log_' + self.testCase_id + '.zip proxy_build_log_' + self.testCase_id, '\$', 5)
			mySSH.copyin(lIpAddr, lUserName, lPassWord, lSourcePath + '/cmake_targets/build_log_' + self.testCase_id + '.zip', '.')
			# don't delete such that we might recover the zips
			#mySSH.command('rm -f build_log_' + self.testCase_id + '.zip','\$', 5)

638 639 640 641 642 643 644 645
		# we do not analyze the logs (we assume the proxy builds fine at this stage),
		# but need to have the following information to correctly display the HTML
		files = {}
		errorandwarnings = {}
		errorandwarnings['errors'] = 0
		errorandwarnings['warnings'] = 0
		errorandwarnings['status'] = True
		files['Target Image Creation'] = errorandwarnings
646 647
		collectInfo = {}
		collectInfo['proxy'] = files
648 649
		mySSH.command('docker image inspect --format=\'Size = {{.Size}} bytes\' proxy:' + tag, '\$', 5)
		result = re.search('Size *= *(?P<size>[0-9\-]+) *bytes', mySSH.getBefore())
650
		allImagesSize = {}
651 652 653
		if result is not None:
			imageSize = float(result.group('size')) / 1000000
			logging.debug('\u001B[1m   proxy size is ' + ('%.0f' % imageSize) + ' Mbytes\u001B[0m')
654
			allImagesSize['proxy'] = str(round(imageSize,1)) + ' Mbytes'
655 656
		else:
			logging.debug('proxy size is unknown')
657
			allImagesSize['proxy'] = 'unknown'
658

659 660 661 662 663 664
		# Cleaning any created tmp volume
		mySSH.command(self.cli + ' volume prune --force || true','\$', 15)
		mySSH.close()

		logging.info('\u001B[1m Building L2sim Proxy Image Pass\u001B[0m')
		HTML.CreateHtmlTestRow('commit ' + tag, 'OK', CONST.ALL_PROCESSES_OK)
665
		HTML.CreateHtmlNextTabHeaderTestRow(collectInfo, allImagesSize)
666

667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
	def Push_Image_to_Local_Registry(self, HTML):
		if self.registrySvrId == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.registrySvrId == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.registrySvrId == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
		logging.debug('Pushing images from server: ' + lIpAddr)
		mySSH = SSH.SSHConnection()
		mySSH.open(lIpAddr, lUserName, lPassWord)
689 690
		imagePrefix = 'porcepix.sboai.cs.eurecom.fr'
		mySSH.command(f'docker login -u oaicicd -p oaicicd {imagePrefix}', '\$', 5)
691
		if re.search('Login Succeeded', mySSH.getBefore()) is None:
692 693
			msg = 'Could not log into local registry'
			logging.error(msg)
694
			mySSH.close()
695
			HTML.CreateHtmlTestRow(msg, 'KO', CONST.ALL_PROCESSES_OK)
696 697 698 699 700
			return False

		orgTag = 'develop'
		if self.ranAllowMerge:
			orgTag = 'ci-temp'
701
		imageNames = ['oai-enb', 'oai-gnb', 'oai-lte-ue', 'oai-nr-ue', 'oai-lte-ru', 'oai-nr-cuup']
702
		for image in imageNames:
703 704 705
			tagToUse = ImageTagToUse(image, self.ranCommitID, self.ranBranch, self.ranAllowMerge)
			mySSH.command(f'docker image tag {image}:{orgTag} {imagePrefix}/{tagToUse}', '\$', 5)
			mySSH.command(f'docker push {imagePrefix}/{tagToUse}', '\$', 120)
706 707
			if re.search(': digest:', mySSH.getBefore()) is None:
				logging.debug(mySSH.getBefore())
708 709
				msg = f'Could not push {image} to local registry : {tagToUse}'
				logging.error(msg)
710
				mySSH.close()
711
				HTML.CreateHtmlTestRow(msg, 'KO', CONST.ALL_PROCESSES_OK)
712
				return False
713
			mySSH.command(f'docker rmi {imagePrefix}/{tagToUse} {image}:{orgTag}', '\$', 30)
714

715
		mySSH.command(f'docker logout {imagePrefix}', '\$', 5)
716
		if re.search('Removing login credentials', mySSH.getBefore()) is None:
717 718
			msg = 'Could not log off from local registry'
			logging.error(msg)
719
			mySSH.close()
720
			HTML.CreateHtmlTestRow(msg, 'KO', CONST.ALL_PROCESSES_OK)
721 722 723
			return False

		mySSH.close()
724
		HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
725 726 727
		return True

	def Pull_Image_from_Local_Registry(self, HTML):
728 729
		# This method can be called either onto a remote server (different from python executor)
		# or directly on the python executor (ie lIpAddr == 'none')
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747
		if self.testSvrId == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.testSvrId == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.testSvrId == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
748
		logging.debug('\u001B[1m Pulling image(s) on server: ' + lIpAddr + '\u001B[0m')
749 750 751
		myCmd = cls_cmd.getConnection(lIpAddr)
		imagePrefix = 'porcepix.sboai.cs.eurecom.fr'
		response = myCmd.run(f'docker login -u oaicicd -p oaicicd {imagePrefix}')
752
		if response.returncode != 0:
753 754
			msg = 'Could not log into local registry'
			logging.error(msg)
755
			myCmd.close()
756
			HTML.CreateHtmlTestRow(msg, 'KO', CONST.ALL_PROCESSES_OK)
757 758
			return False
		for image in self.imageToPull:
759 760
			tagToUse = ImageTagToUse(image, self.ranCommitID, self.ranBranch, self.ranAllowMerge)
			cmd = f'docker pull {imagePrefix}/{tagToUse}'
761
			response = myCmd.run(cmd, timeout=120)
762
			if response.returncode != 0:
763 764 765
				logging.debug(response)
				msg = f'Could not pull {image} from local registry : {tagToUse}'
				logging.error(msg)
766
				myCmd.close()
767
				HTML.CreateHtmlTestRow('msg', 'KO', CONST.ALL_PROCESSES_OK)
768
				return False
769 770 771
			myCmd.run(f'docker tag {imagePrefix}/{tagToUse} oai-ci/{tagToUse}')
			myCmd.run(f'docker rmi {imagePrefix}/{tagToUse}')
		response = myCmd.run(f'docker logout {imagePrefix}')
772
		if response.returncode != 0:
773 774
			msg = 'Could not log off from local registry'
			logging.error(msg)
775
			myCmd.close()
776
			HTML.CreateHtmlTestRow(msg, 'KO', CONST.ALL_PROCESSES_OK)
777
			return False
778
		myCmd.close()
779
		HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
780 781 782
		return True

	def Clean_Test_Server_Images(self, HTML):
783 784
		# This method can be called either onto a remote server (different from python executor)
		# or directly on the python executor (ie lIpAddr == 'none')
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
		if self.testSvrId == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.testSvrId == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.testSvrId == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
803 804
		if lIpAddr != 'none':
			logging.debug('Removing test images from server: ' + lIpAddr)
805
			myCmd = cls_cmd.RemoteCmd(lIpAddr)
806 807
		else:
			logging.debug('Removing test images locally')
808
			myCmd = cls_cmd.LocalCmd()
809

810
		for image in IMAGES:
811 812
			imageTag = ImageTagToUse(image, self.ranCommitID, self.ranBranch, self.ranAllowMerge)
			cmd = f'docker rmi oai-ci/{imageTag}'
813
			myCmd.run(cmd, reportNonZero=False)
814

815
		myCmd.close()
816
		HTML.CreateHtmlTestRow('N/A', 'OK', CONST.ALL_PROCESSES_OK)
817 818
		return True

Raphael Defosseux's avatar
Raphael Defosseux committed
819
	def DeployObject(self, HTML, EPC):
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
		if self.eNB_serverId[self.eNB_instance] == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
		logging.debug('\u001B[1m Deploying OAI Object on server: ' + lIpAddr + '\u001B[0m')
839
		self.deployKind[self.eNB_instance] = True
840

841 842
		mySSH = SSH.SSHConnection()
		mySSH.open(lIpAddr, lUserName, lPassWord)
843

844
		CreateWorkspace(mySSH, lSourcePath, self.ranRepository, self.ranCommitID, self.ranTargetBranch, self.ranAllowMerge)
845

846
		mySSH.command('cd ' + lSourcePath + '/' + self.yamlPath[self.eNB_instance], '\$', 5)
847
		mySSH.command('cp docker-compose.y*ml ci-docker-compose.yml', '\$', 5)
848
		for image in IMAGES:
849 850
			imageTag = ImageTagToUse(image, self.ranCommitID, self.ranBranch, self.ranAllowMerge)
			mySSH.command(f'sed -i -e "s#image: {image}:latest#image: oai-ci/{imageTag}#" ci-docker-compose.yml', '\$', 2)
851

852
		# Currently support only one
853 854 855 856 857
		svcName = self.services[self.eNB_instance]
		if svcName == '':
			logging.warning('no service name given: starting all services in ci-docker-compose.yml!')

		mySSH.command(f'docker-compose --file ci-docker-compose.yml up -d -- {svcName}', '\$', 30)
858 859

		# Checking Status
860 861 862
		grep = ''
		if svcName != '': grep = f' | grep -A3 {svcName}'
		mySSH.command(f'docker-compose --file ci-docker-compose.yml config {grep}', '\$', 5)
863 864 865 866 867
		result = re.search('container_name: (?P<container_name>[a-zA-Z0-9\-\_]+)', mySSH.getBefore())
		unhealthyNb = 0
		healthyNb = 0
		startingNb = 0
		containerName = ''
868 869
		usedImage = ''
		imageInfo = ''
870 871 872 873 874
		if result is not None:
			containerName = result.group('container_name')
			time.sleep(5)
			cnt = 0
			while (cnt < 3):
875
				mySSH.command('docker inspect --format="{{.State.Health.Status}}" ' + containerName, '\$', 5)
876 877 878 879 880 881 882 883
				unhealthyNb = mySSH.getBefore().count('unhealthy')
				healthyNb = mySSH.getBefore().count('healthy') - unhealthyNb
				startingNb = mySSH.getBefore().count('starting')
				if healthyNb == 1:
					cnt = 10
				else:
					time.sleep(10)
					cnt += 1
884

885 886
			mySSH.command('docker inspect --format="ImageUsed: {{.Config.Image}}" ' + containerName, '\$', 5)
			for stdoutLine in mySSH.getBefore().split('\n'):
887 888
				if stdoutLine.count('ImageUsed: oai-ci'):
					usedImage = stdoutLine.replace('ImageUsed: oai-ci', 'oai-ci').strip()
889 890 891 892 893 894 895 896 897 898
					logging.debug('Used image is ' + usedImage)
			if usedImage != '':
				mySSH.command('docker image inspect --format "* Size     = {{.Size}} bytes\n* Creation = {{.Created}}\n* Id       = {{.Id}}" ' + usedImage, '\$', 5, silent=True)
				for stdoutLine in mySSH.getBefore().split('\n'):
					if re.search('Size     = [0-9]', stdoutLine) is not None:
						imageInfo += stdoutLine.strip() + '\n'
					if re.search('Creation = [0-9]', stdoutLine) is not None:
						imageInfo += stdoutLine.strip() + '\n'
					if re.search('Id       = sha256', stdoutLine) is not None:
						imageInfo += stdoutLine.strip() + '\n'
899 900 901 902
		logging.debug(' -- ' + str(healthyNb) + ' healthy container(s)')
		logging.debug(' -- ' + str(unhealthyNb) + ' unhealthy container(s)')
		logging.debug(' -- ' + str(startingNb) + ' still starting container(s)')

903 904 905
		self.testCase_id = HTML.testCase_id
		self.eNB_logFile[self.eNB_instance] = 'enb_' + self.testCase_id + '.log'

906 907 908 909
		status = False
		if healthyNb == 1:
			cnt = 0
			while (cnt < 20):
910
				mySSH.command('docker logs ' + containerName + ' | egrep --text --color=never -i "wait|sync|Starting"', '\$', 30)
911
				result = re.search('got sync|Starting E1AP at CU UP|Starting F1AP at CU|Got sync|Waiting for RUs to be configured', mySSH.getBefore())
912 913 914 915 916 917 918 919
				if result is None:
					time.sleep(6)
					cnt += 1
				else:
					cnt = 100
					status = True
					logging.info('\u001B[1m Deploying OAI object Pass\u001B[0m')
					time.sleep(10)
920 921 922
		else:
			# containers are unhealthy, so we won't start. However, logs are stored at the end
			# in UndeployObject so we here store the logs of the unhealthy container to report it
923 924
			logfilename = f'{lSourcePath}/cmake_targets/log/{self.eNB_logFile[self.eNB_instance]}'
			mySSH.command(f'docker logs {containerName} > {logfilename}', '\$', 30)
925
			mySSH.copyin(lIpAddr, lUserName, lPassWord, logfilename, '.')
926 927
		mySSH.close()

928
		message = ''
929
		if usedImage != '':
930 931
			message += f'Used Image = {usedImage} :\n'
			message += imageInfo
932
		else:
933
			message += 'Could not retrieve used image info!\n'
934
		if status:
935
			message += '\nHealthy deployment!\n'
936
		else:
937
			message += '\nUnhealthy deployment! -- Check logs for reason!\n'
Raphael Defosseux's avatar
Raphael Defosseux committed
938
		if status:
939
			HTML.CreateHtmlTestRowQueue('N/A', 'OK', [message])
Raphael Defosseux's avatar
Raphael Defosseux committed
940
		else:
941
			self.exitStatus = 1
942
			HTML.CreateHtmlTestRowQueue('N/A', 'KO', [message])
943

944

Raphael Defosseux's avatar
Raphael Defosseux committed
945
	def UndeployObject(self, HTML, RAN):
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963
		if self.eNB_serverId[self.eNB_instance] == '0':
			lIpAddr = self.eNBIPAddress
			lUserName = self.eNBUserName
			lPassWord = self.eNBPassword
			lSourcePath = self.eNBSourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '1':
			lIpAddr = self.eNB1IPAddress
			lUserName = self.eNB1UserName
			lPassWord = self.eNB1Password
			lSourcePath = self.eNB1SourceCodePath
		elif self.eNB_serverId[self.eNB_instance] == '2':
			lIpAddr = self.eNB2IPAddress
			lUserName = self.eNB2UserName
			lPassWord = self.eNB2Password
			lSourcePath = self.eNB2SourceCodePath
		if lIpAddr == '' or lUserName == '' or lPassWord == '' or lSourcePath == '':
			HELP.GenericHelp(CONST.Version)
			sys.exit('Insufficient Parameter')
964
		logging.debug('\u001B[1m Undeploying OAI Object from server: ' + lIpAddr + '\u001B[0m')
965 966
		mySSH = SSH.SSHConnection()
		mySSH.open(lIpAddr, lUserName, lPassWord)
967

968
		mySSH.command('cd ' + lSourcePath + '/' + self.yamlPath[self.eNB_instance], '\$', 5)
969 970 971 972 973 974 975 976 977 978 979 980 981

		svcName = self.services[self.eNB_instance]
		forceDown = False
		if svcName != '':
			logging.warning(f'service name given, but will stop all services in ci-docker-compose.yml!')
			svcName = ''

		mySSH.command(f'docker-compose -f ci-docker-compose.yml config --services', '\$', 5)
		# first line has command, last line has next command prompt
		allServices = mySSH.getBefore().split('\r\n')[1:-1]
		services = []
		for s in allServices:
			mySSH.command(f'docker-compose -f ci-docker-compose.yml ps --all -- {s}', '\$', 5, silent=False)
982
			running = mySSH.getBefore().split('\r\n')[2:-1]
983
			logging.debug(f'running services: {running}')
984 985 986 987
			if len(running) > 0: # something is running for that service
				services.append(s)
		logging.info(f'stopping services {services}')

988
		mySSH.command(f'docker-compose -f ci-docker-compose.yml stop -t3', '\$', 30)
989 990 991 992
		time.sleep(5)  # give some time to running containers to stop
		for svcName in services:
			# head -n -1 suppresses the final "X exited with status code Y"
			filename = f'{svcName}-{HTML.testCase_id}.log'
993
			mySSH.command(f'docker-compose -f ci-docker-compose.yml logs --no-log-prefix -- {svcName} &> {lSourcePath}/cmake_targets/log/{filename}', '\$', 120)
994

995
		mySSH.command('docker-compose -f ci-docker-compose.yml down -v', '\$', 5)
996
		mySSH.close()
997

998
		# Analyzing log file!
999 1000 1001 1002 1003 1004 1005 1006
		files = ','.join([f'{s}-{HTML.testCase_id}' for s in services])
		if len(services) > 1:
			files = '{' + files + '}'
		copyin_res = 0
		if len(services) > 0:
			copyin_res = mySSH.copyin(lIpAddr, lUserName, lPassWord, f'{lSourcePath}/cmake_targets/log/{files}.log', '.')
		if copyin_res == -1:
			HTML.htmleNBFailureMsg='Could not copy logfile to analyze it!'
Raphael Defosseux's avatar
Raphael Defosseux committed
1007
			HTML.CreateHtmlTestRow('N/A', 'KO', CONST.ENB_PROCESS_NOLOGFILE_TO_ANALYZE)
1008
			self.exitStatus = 1
1009
		# use function for UE log analysis, when oai-nr-ue container is used
1010
		elif 'oai-nr-ue' in services or 'lte_ue0' in services:
1011 1012 1013 1014 1015 1016 1017 1018
			self.exitStatus == 0
			logging.debug('\u001B[1m Analyzing UE logfile ' + filename + ' \u001B[0m')
			logStatus = cls_oaicitest.OaiCiTest().AnalyzeLogFile_UE(f'{filename}', HTML, RAN)
			if (logStatus < 0):
				fullStatus = False
				HTML.CreateHtmlTestRow('UE log Analysis', 'KO', logStatus)
			else:
				HTML.CreateHtmlTestRow('UE log Analysis', 'OK', CONST.ALL_PROCESSES_OK)
Raphael Defosseux's avatar
Raphael Defosseux committed
1019
		else:
1020 1021 1022 1023 1024 1025 1026 1027 1028
			for svcName in services:
				filename = f'{svcName}-{HTML.testCase_id}.log'
				logging.debug(f'\u001B[1m Analyzing logfile {filename}\u001B[0m')
				logStatus = RAN.AnalyzeLogFile_eNB(filename, HTML, self.ran_checkers)
				if (logStatus < 0):
					HTML.CreateHtmlTestRow(RAN.runtime_stats, 'KO', logStatus)
					self.exitStatus = 1
				else:
					HTML.CreateHtmlTestRow(RAN.runtime_stats, 'OK', CONST.ALL_PROCESSES_OK)
1029
			# all the xNB run logs shall be on the server 0 for logCollecting
1030 1031 1032 1033 1034 1035
			if self.eNB_serverId[self.eNB_instance] != '0':
				mySSH.copyout(self.eNBIPAddress, self.eNBUserName, self.eNBPassword, f'./{files}.log', f'{self.eNBSourceCodePath}/cmake_targets/')
		if self.exitStatus == 0:
			logging.info('\u001B[1m Undeploying OAI Object Pass\u001B[0m')
		else:
			logging.error('\u001B[1m Undeploying OAI Object Failed\u001B[0m')
1036

1037
	def DeployGenObject(self, HTML, RAN, UE):
1038
		self.exitStatus = 0
1039
		logging.debug('\u001B[1m Checking Services to deploy\u001B[0m')
1040 1041
		# Implicitly we are running locally
		myCmd = cls_cmd.LocalCmd(d = self.yamlPath[0])
1042
		self.deployKind[0] = False
1043 1044 1045 1046
		cmd = 'docker-compose config --services'
		listServices = myCmd.run(cmd)
		if listServices.returncode != 0:
			myCmd.close()
1047 1048 1049 1050
			self.exitStatus = 1
			HTML.CreateHtmlTestRow('SVC not Found', 'KO', CONST.ALL_PROCESSES_OK)
			return
		for reqSvc in self.services[0].split(' '):
1051
			res = re.search(reqSvc, listServices.stdout)
1052 1053 1054 1055
			if res is None:
				logging.error(reqSvc + ' not found in specified docker-compose')
				self.exitStatus = 1
		if (self.exitStatus == 1):
1056
			myCmd.close()
1057 1058
			HTML.CreateHtmlTestRow('SVC not Found', 'KO', CONST.ALL_PROCESSES_OK)
			return
1059
		cmd = 'cp docker-compose.y*ml docker-compose-ci.yml'
1060
		myCmd.run(cmd, silent=self.displayedNewTags)
1061
		imageNames = ['oai-enb', 'oai-gnb', 'oai-lte-ue', 'oai-nr-ue', 'oai-lte-ru', 'oai-nr-cuup']
1062
		for image in imageNames:
1063 1064
			tagToUse = ImageTagToUse(image, self.ranCommitID, self.ranBranch, self.ranAllowMerge)
			cmd = f'sed -i -e "s@oaisoftwarealliance/{image}:develop@oai-ci/{tagToUse}@" docker-compose-ci.yml'
1065
			myCmd.run(cmd, silent=self.displayedNewTags)
1066 1067
		self.displayedNewTags = True

1068
		cmd = f'docker-compose -f docker-compose-ci.yml up -d {self.services[0]}'
1069 1070 1071
		deployStatus = myCmd.run(cmd, timeout=100)
		if deployStatus.returncode != 0:
			myCmd.close()
1072 1073 1074 1075 1076
			self.exitStatus = 1
			logging.error('Could not deploy')
			HTML.CreateHtmlTestRow('Could not deploy', 'KO', CONST.ALL_PROCESSES_OK)
			return

1077
		logging.debug('\u001B[1m Checking if all deployed healthy\u001B[0m')
1078
		cmd = 'docker-compose -f docker-compose-ci.yml ps -a'
1079 1080
		count = 0
		healthy = 0
1081
		restarting = 0
1082
		newContainers = []
1083 1084
		while (count < 10):
			count += 1
1085
			containerStatus = []
1086
			deployStatus = myCmd.run(cmd, 30, silent=True)
1087
			healthy = 0
1088
			restarting = 0
1089
			for state in deployStatus.stdout.split('\n'):
1090 1091
				state = state.strip()
				res = re.search('Name|NAME|----------', state)
1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
				if res is not None:
					continue
				if len(state) == 0:
					continue
				res = re.search('^(?P<container_name>[a-zA-Z0-9\-\_]+) ', state)
				if res is not None:
					cName = res.group('container_name')
					found = False
					for alreadyDeployed in self.deployedContainers:
						if cName == alreadyDeployed:
							found = True
					if not found:
						newContainers.append(cName)
						self.deployedContainers.append(cName)
1106
				if re.search('\(healthy\)', state) is not None:
1107
					healthy += 1
1108
				if re.search('rfsim4g-db-init.*Exit 0', state) is not None or re.search('rfsim4g-db-init.*Exited \(0\)', state) is not None:
1109
					myCmd.run('docker rm -f rfsim4g-db-init', timeout=30, silent=True, reportNonZero=False)
1110 1111
				if re.search('l2sim4g-db-init.*Exit 0', state) is not None or re.search('l2sim4g-db-init.*Exited \(0\)', state) is not None:
					myCmd.run('docker rm -f l2sim4g-db-init', timeout=30, silent=True, reportNonZero=False)
1112 1113 1114 1115 1116
				if re.search('Restarting', state) is None:
					containerStatus.append(state)
				else:
					restarting += 1
			if healthy == self.nb_healthy[0] and restarting == 0:
1117 1118 1119 1120
				count = 100
			else:
				time.sleep(10)

1121
		html_cell = ''
1122
		for newCont in newContainers:
1123 1124
			if newCont == 'rfsim4g-db-init':
				continue
1125
			cmd = 'docker inspect -f "{{.Config.Image}}" ' + newCont
1126 1127
			imageInspect = myCmd.run(cmd, timeout=30, silent=True)
			imageName = str(imageInspect.stdout).strip()
1128
			cmd = 'docker image inspect --format \'{{.RepoTags}}\t{{.Size}} bytes\t{{index (split .Created ".") 0}}\n{{.Id}}\' ' + imageName
1129
			imageInspect = myCmd.run(cmd, 30, silent=True)
1130
			html_cell += imageInspect.stdout + '\n'
1131
		myCmd.close()
1132 1133 1134

		for cState in containerStatus:
			html_cell += cState + '\n'
1135
		if count == 100 and healthy == self.nb_healthy[0]:
1136 1137 1138
			if self.tsharkStarted == False:
				logging.debug('Starting tshark on public network')
				self.CaptureOnDockerNetworks()
1139
			HTML.CreateHtmlTestRowQueue('n/a', 'OK', [html_cell])
1140 1141
			for cState in containerStatus:
				logging.debug(cState)
1142 1143
			logging.info('\u001B[1m Deploying OAI Object(s) PASS\u001B[0m')
		else:
1144
			HTML.CreateHtmlTestRowQueue('Could not deploy in time', 'KO', [html_cell])
1145 1146
			for cState in containerStatus:
				logging.debug(cState)
1147
			logging.error('\u001B[1m Deploying OAI Object(s) FAILED\u001B[0m')
1148 1149 1150 1151 1152 1153 1154
			HTML.testCase_id = 'AUTO-UNDEPLOY'
			UE.testCase_id = 'AUTO-UNDEPLOY'
			HTML.desc = 'Automatic Undeployment'
			UE.desc = 'Automatic Undeployment'
			UE.ShowTestID()
			self.UndeployGenObject(HTML, RAN, UE)
			self.exitStatus = 1
1155

1156 1157 1158 1159 1160 1161
	# 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
1162 1163 1164 1165 1166
	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

1167
	def CaptureOnDockerNetworks(self):
1168 1169 1170 1171
		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: @@"'
		networkNames = myCmd.run(cmd, silent=True)
		myCmd.close()
1172
		# Allow only: control plane RAN (SCTP), HTTP of control in CN (port 80), PFCP traffic (port 8805), MySQL (port 3306)
1173
		capture_filter = 'sctp or port 80 or port 8805 or icmp or port 3306'
1174
		interfaces = []
1175
		iInterfaces = ''
1176
		for name in networkNames.stdout.split('\n'):
1177
			if re.search('rfsim', name) is not None or re.search('l2sim', name) is not None:
1178
				interfaces.append(name)
1179
				iInterfaces += f'-i {name} '
1180
		ymlPath = self.yamlPath[0].split('/')
1181
		output_file = f'/tmp/capture_{ymlPath[1]}.pcap'
1182
		self.tsharkStarted = True
1183 1184 1185 1186 1187 1188 1189 1190
		# On old systems (ubuntu 18), pyshark live-capture is buggy.
		# Going back to old method
		if sys.version_info < (3, 7):
			cmd = f'nohup tshark -f "{capture_filter}" {iInterfaces} -w {output_file} > /tmp/tshark.log 2>&1 &'
			myCmd = cls_cmd.LocalCmd()
			myCmd.run(cmd, timeout=5, reportNonZero=False)
			myCmd.close()
			return
1191 1192 1193
		x = threading.Thread(target = self.LaunchPySharkCapture, args = (interfaces,capture_filter,output_file,))
		x.daemon = True
		x.start()
1194

1195
	def UndeployGenObject(self, HTML, RAN, UE):
1196
		self.exitStatus = 0
1197
		# Implicitly we are running locally
1198 1199
		ymlPath = self.yamlPath[0].split('/')
		logPath = '../cmake_targets/log/' + ymlPath[1]
1200 1201
		myCmd = cls_cmd.LocalCmd(d = self.yamlPath[0])
		cmd = 'cp docker-compose.y*ml docker-compose-ci.yml'
1202
		myCmd.run(cmd, silent=self.displayedNewTags)
1203
		for image in IMAGES:
1204 1205
			tagToUse = ImageTagToUse(image, self.ranCommitID, self.ranBranch, self.ranAllowMerge)
			cmd = f'sed -i -e "s@oaisoftwarealliance/{image}:develop@oai-ci/{tagToUse}@" docker-compose-ci.yml'
1206
			myCmd.run(cmd, silent=self.displayedNewTags)
1207
		self.displayedNewTags = True
1208

1209
		# check which containers are running for log recovery later
1210 1211
		cmd = 'docker-compose -f docker-compose-ci.yml ps --all'
		deployStatusLogs = myCmd.run(cmd, timeout=30)
1212 1213 1214

		# Stop the containers to shut down objects
		logging.debug('\u001B[1m Stopping containers \u001B[0m')
1215 1216 1217 1218
		cmd = 'docker-compose -f docker-compose-ci.yml stop -t3'
		deployStatus = myCmd.run(cmd, timeout=100)
		if deployStatus.returncode != 0:
			myCmd.close()
1219 1220 1221 1222 1223 1224
			self.exitStatus = 1
			logging.error('Could not stop containers')
			HTML.CreateHtmlTestRow('Could not stop', 'KO', CONST.ALL_PROCESSES_OK)
			logging.error('\u001B[1m Undeploying OAI Object(s) FAILED\u001B[0m')
			return

1225
		anyLogs = False
1226
		logging.debug('Working dir is now . (ie ci-scripts)')
1227 1228 1229
		myCmd2 = cls_cmd.LocalCmd()
		myCmd2.run(f'mkdir -p {logPath}')
		myCmd2.cd(logPath)
1230
		for state in deployStatusLogs.stdout.split('\n'):
1231
			res = re.search('Name|NAME|----------', state)
1232 1233 1234 1235 1236 1237 1238 1239
			if res is not None:
				continue
			if len(state) == 0:
				continue
			res = re.search('^(?P<container_name>[a-zA-Z0-9\-\_]+) ', state)
			if res is not None:
				anyLogs = True
				cName = res.group('container_name')
1240 1241
				cmd = f'docker logs {cName} > {cName}.log 2>&1'
				myCmd2.run(cmd, timeout=30, reportNonZero=False)
1242
				if re.search('magma-mme', cName) is not None:
1243 1244
					cmd = f'docker cp -L {cName}:/var/log/mme.log {cName}-full.log'
					myCmd2.run(cmd, timeout=30, reportNonZero=False)
1245
		fullStatus = True
1246
		if anyLogs:
1247
			# Analyzing log file(s)!
1248 1249
			listOfPossibleRanContainers = ['enb', 'gnb', 'cu', 'du']
			for container in listOfPossibleRanContainers:
1250
				filenames = './*-oai-' + container + '.log'
1251 1252
				cmd = f'ls {filenames}'
				lsStatus = myCmd2.run(cmd, silent=True, reportNonZero=False)
1253
				if lsStatus.returncode != 0:
1254
					continue
1255
				filenames = str(lsStatus.stdout).strip()
1256

1257 1258
				for filename in filenames.split('\n'):
					logging.debug('\u001B[1m Analyzing xNB logfile ' + filename + ' \u001B[0m')
1259
					logStatus = RAN.AnalyzeLogFile_eNB(f'{logPath}/{filename}', HTML, self.ran_checkers)
1260 1261 1262 1263 1264 1265
					if (logStatus < 0):
						fullStatus = False
						self.exitStatus = 1
						HTML.CreateHtmlTestRow(RAN.runtime_stats, 'KO', logStatus)
					else:
						HTML.CreateHtmlTestRow(RAN.runtime_stats, 'OK', CONST.ALL_PROCESSES_OK)
1266

1267 1268
			listOfPossibleUeContainers = ['lte-ue*', 'nr-ue*']
			for container in listOfPossibleUeContainers:
1269
				filenames = './*-oai-' + container + '.log'
1270 1271
				cmd = f'ls {filenames}'
				lsStatus = myCmd2.run(cmd, silent=True, reportNonZero=False)
1272
				if lsStatus.returncode != 0:
1273
					continue
1274
				filenames = str(lsStatus.stdout).strip()
1275

1276 1277
				for filename in filenames.split('\n'):
					logging.debug('\u001B[1m Analyzing UE logfile ' + filename + ' \u001B[0m')
1278
					logStatus = UE.AnalyzeLogFile_UE(f'{logPath}/{filename}', HTML, RAN)
1279 1280 1281 1282 1283
					if (logStatus < 0):
						fullStatus = False
						HTML.CreateHtmlTestRow('UE log Analysis', 'KO', logStatus)
					else:
						HTML.CreateHtmlTestRow('UE log Analysis', 'OK', CONST.ALL_PROCESSES_OK)
1284

1285 1286
			if self.tsharkStarted:
				self.tsharkStarted = True
1287
				cmd = 'killall tshark'
1288 1289 1290 1291
				myCmd2.run(cmd, reportNonZero=False)
				cmd = 'killall dumpcap'
				myCmd2.run(cmd, reportNonZero=False)
				time.sleep(5)
1292
				ymlPath = self.yamlPath[0].split('/')
1293
				# The working dir is still logPath
1294 1295
				cmd = f'mv /tmp/capture_{ymlPath[1]}.pcap .'
				myCmd2.run(cmd, timeout=100, reportNonZero=False)
1296
				self.tsharkStarted = False
1297
		myCmd2.close()
1298

1299
		logging.debug('\u001B[1m Undeploying \u001B[0m')
1300
		logging.debug(f'Working dir is back {self.yamlPath[0]}')
1301
		cmd = 'docker-compose -f docker-compose-ci.yml down -v'
1302 1303 1304
		deployStatus = myCmd.run(cmd, timeout=100)
		if deployStatus.returncode != 0:
			myCmd.close()
1305 1306 1307 1308 1309 1310
			self.exitStatus = 1
			logging.error('Could not undeploy')
			HTML.CreateHtmlTestRow('Could not undeploy', 'KO', CONST.ALL_PROCESSES_OK)
			logging.error('\u001B[1m Undeploying OAI Object(s) FAILED\u001B[0m')
			return

1311
		self.deployedContainers = []
1312
		myCmd.close()
1313

1314 1315 1316 1317 1318
		if fullStatus:
			HTML.CreateHtmlTestRow('n/a', 'OK', CONST.ALL_PROCESSES_OK)
			logging.info('\u001B[1m Undeploying OAI Object(s) PASS\u001B[0m')
		else:
			HTML.CreateHtmlTestRow('n/a', 'KO', CONST.ALL_PROCESSES_OK)
1319
			logging.error('\u001B[1m Undeploying OAI Object(s) FAIL\u001B[0m')
1320

1321 1322 1323 1324 1325 1326
	def StatsFromGenObject(self, HTML):
		self.exitStatus = 0
		ymlPath = self.yamlPath[0].split('/')
		logPath = '../cmake_targets/log/' + ymlPath[1]

		# if the containers are running, recover the logs!
1327 1328 1329
		myCmd = cls_cmd.LocalCmd(d = self.yamlPath[0])
		cmd = 'docker-compose -f docker-compose-ci.yml ps --all'
		deployStatus = myCmd.run(cmd, timeout=30)
1330 1331
		cmd = 'docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" '
		anyLogs = False
1332
		for state in deployStatus.stdout.split('\n'):
1333
			res = re.search('Name|NAME|----------', state)
1334 1335 1336 1337 1338 1339 1340 1341 1342 1343
			if res is not None:
				continue
			if len(state) == 0:
				continue
			res = re.search('^(?P<container_name>[a-zA-Z0-9\-\_]+) ', state)
			if res is not None:
				anyLogs = True
				cmd += res.group('container_name') + ' '
		message = ''
		if anyLogs:
1344 1345
			stats = myCmd.run(cmd, timeout=30)
			for statLine in stats.stdout.split('\n'):
1346 1347
				logging.debug(statLine)
				message += statLine + '\n'
1348
		myCmd.close()
1349

1350
		HTML.CreateHtmlTestRowQueue(self.pingOptions, 'OK', [message])
1351

1352
	def PingExit(self, HTML, RAN, UE, status, message):
1353
		if status:
1354
			HTML.CreateHtmlTestRowQueue(self.pingOptions, 'OK', [message])
1355
		else:
1356
			logging.error('\u001B[1;37;41m ping test FAIL -- ' + message + ' \u001B[0m')
1357
			HTML.CreateHtmlTestRowQueue(self.pingOptions, 'KO', [message])
1358
			# Automatic undeployment
1359 1360 1361
			logging.warning('----------------------------------------')
			logging.warning('\u001B[1m Starting Automatic undeployment \u001B[0m')
			logging.warning('----------------------------------------')
1362 1363
			HTML.testCase_id = 'AUTO-UNDEPLOY'
			HTML.desc = 'Automatic Un-Deployment'
1364
			self.UndeployGenObject(HTML, RAN, UE)
1365
			self.exitStatus = 1
1366

1367
	def IperfFromContainer(self, HTML, RAN, UE):
1368
		myCmd = cls_cmd.LocalCmd()
1369 1370
		self.exitStatus = 0

1371 1372
		ymlPath = self.yamlPath[0].split('/')
		logPath = '../cmake_targets/log/' + ymlPath[1]
1373
		cmd = f'mkdir -p {logPath}'
1374
		myCmd.run(cmd, silent=True)
1375 1376

		# Start the server process
1377
		cmd = f'docker exec -d {self.svrContName} /bin/bash -c "nohup iperf {self.svrOptions} > /tmp/iperf_server.log 2>&1"'
1378
		myCmd.run(cmd)
1379
		time.sleep(3)
1380 1381

		# Start the client process
1382
		cmd = f'docker exec {self.cliContName} /bin/bash -c "iperf {self.cliOptions}" 2>&1 | tee {logPath}/iperf_client_{HTML.testCase_id}.log'
1383
		clientStatus = myCmd.run(cmd, timeout=100)
1384 1385

		# Stop the server process
1386
		cmd = f'docker exec {self.svrContName} /bin/bash -c "pkill iperf"'
1387
		myCmd.run(cmd)
1388 1389 1390
		time.sleep(3)
		serverStatusFilename = f'{logPath}/iperf_server_{HTML.testCase_id}.log'
		cmd = f'docker cp {self.svrContName}:/tmp/iperf_server.log {serverStatusFilename}'
1391 1392
		myCmd.run(cmd, timeout=60)
		myCmd.close()
1393

1394 1395 1396 1397
		# clientStatus was retrieved above. The serverStatus was
		# written in the background, then copied to the local machine
		with open(serverStatusFilename, 'r') as f:
			serverStatus = f.read()
1398
		(iperfStatus, msg) = AnalyzeIperf(self.cliOptions, clientStatus.stdout, serverStatus)
1399 1400
		if iperfStatus:
			logging.info('\u001B[1m Iperf Test PASS\u001B[0m')
1401 1402
		else:
			logging.error('\u001B[1;37;41m Iperf Test FAIL\u001B[0m')
1403
		self.IperfExit(HTML, RAN, UE, iperfStatus, msg)
1404

1405
	def IperfExit(self, HTML, RAN, UE, status, message):
1406
		html_cell = f'UE\n{message}'
1407
		if status:
1408
			HTML.CreateHtmlTestRowQueue(self.cliOptions, 'OK', [html_cell])
1409
		else:
1410
			logging.error('\u001B[1m Iperf Test FAIL -- ' + message + ' \u001B[0m')
1411
			HTML.CreateHtmlTestRowQueue(self.cliOptions, 'KO', [html_cell])
1412 1413 1414 1415 1416 1417 1418 1419
			# Automatic undeployment
			logging.warning('----------------------------------------')
			logging.warning('\u001B[1m Starting Automatic undeployment \u001B[0m')
			logging.warning('----------------------------------------')
			HTML.testCase_id = 'AUTO-UNDEPLOY'
			HTML.desc = 'Automatic Un-Deployment'
			self.UndeployGenObject(HTML, RAN, UE)
			self.exitStatus = 1
1420

Laurent THOMAS's avatar
Laurent THOMAS committed
1421

1422 1423 1424 1425 1426 1427
	def CheckAndAddRoute(self, svrName, ipAddr, userName, password):
		logging.debug('Checking IP routing on ' + svrName)
		mySSH = SSH.SSHConnection()
		if svrName == 'porcepix':
			mySSH.open(ipAddr, userName, password)
			# Check if route to asterix gnb exists
1428
			mySSH.command('ip route | grep --colour=never "192.168.68.64/26"', '\$', 10)
1429
			result = re.search('172.21.16.127', mySSH.getBefore())
1430
			if result is None:
1431
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.64/26 via 172.21.16.127 dev eno1', '\$', 10)
1432
			# Check if route to obelix enb exists
1433
			mySSH.command('ip route | grep --colour=never "192.168.68.128/26"', '\$', 10)
1434
			result = re.search('172.21.16.128', mySSH.getBefore())
1435
			if result is None:
1436
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.128/26 via 172.21.16.128 dev eno1', '\$', 10)
1437 1438 1439 1440 1441 1442
			# Check if forwarding is enabled
			mySSH.command('sysctl net.ipv4.conf.all.forwarding', '\$', 10)
			result = re.search('net.ipv4.conf.all.forwarding = 1', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
			# Check if iptables forwarding is accepted
1443
			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
1444 1445 1446 1447 1448 1449 1450
			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
			mySSH.close()
		if svrName == 'asterix':
			mySSH.open(ipAddr, userName, password)
			# Check if route to porcepix epc exists
1451
			mySSH.command('ip route | grep --colour=never "192.168.61.192/26"', '\$', 10)
1452
			result = re.search('172.21.16.136', mySSH.getBefore())
1453
			if result is None:
1454
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.61.192/26 via 172.21.16.136 dev em1', '\$', 10)
1455
			# Check if route to porcepix cn5g exists
1456
			mySSH.command('ip route | grep --colour=never "192.168.70.128/26"', '\$', 10)
1457
			result = re.search('172.21.16.136', mySSH.getBefore())
1458
			if result is None:
1459
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.70.128/26 via 172.21.16.136 dev em1', '\$', 10)
1460
			# Check if X2 route to obelix enb exists
1461
			mySSH.command('ip route | grep --colour=never "192.168.68.128/26"', '\$', 10)
1462
			result = re.search('172.21.16.128', mySSH.getBefore())
1463
			if result is None:
1464
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.128/26 via 172.21.16.128 dev em1', '\$', 10)
1465 1466 1467 1468 1469 1470
			# Check if forwarding is enabled
			mySSH.command('sysctl net.ipv4.conf.all.forwarding', '\$', 10)
			result = re.search('net.ipv4.conf.all.forwarding = 1', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
			# Check if iptables forwarding is accepted
1471
			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
1472 1473 1474 1475 1476 1477 1478
			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
			mySSH.close()
		if svrName == 'obelix':
			mySSH.open(ipAddr, userName, password)
			# Check if route to porcepix epc exists
1479
			mySSH.command('ip route | grep --colour=never "192.168.61.192/26"', '\$', 10)
1480
			result = re.search('172.21.16.136', mySSH.getBefore())
1481
			if result is None:
1482
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.61.192/26 via 172.21.16.136 dev eno1', '\$', 10)
1483
			# Check if X2 route to asterix gnb exists
1484
			mySSH.command('ip route | grep --colour=never "192.168.68.64/26"', '\$', 10)
1485
			result = re.search('172.21.16.127', mySSH.getBefore())
1486
			if result is None:
1487
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.64/26 via 172.21.16.127 dev eno1', '\$', 10)
1488
			# Check if X2 route to nepes gnb exists
1489
			mySSH.command('ip route | grep --colour=never "192.168.68.192/26"', '\$', 10)
1490
			result = re.search('172.21.16.137', mySSH.getBefore())
1491
			if result is None:
1492
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.192/26 via 172.21.16.137 dev eno1', '\$', 10)
1493 1494 1495 1496 1497 1498
			# Check if forwarding is enabled
			mySSH.command('sysctl net.ipv4.conf.all.forwarding', '\$', 10)
			result = re.search('net.ipv4.conf.all.forwarding = 1', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
			# Check if iptables forwarding is accepted
1499
			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
1500 1501 1502 1503 1504 1505
			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
			mySSH.close()
		if svrName == 'nepes':
			mySSH.open(ipAddr, userName, password)
1506 1507 1508
			# Check if route to ofqot gnb exists
			mySSH.command('ip route | grep --colour=never "192.168.68.192/26"', '\$', 10)
			result = re.search('172.21.16.109', mySSH.getBefore())
1509
			if result is None:
1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.192/26 via 172.21.16.109 dev enp0s31f6', '\$', 10)
			mySSH.command('sysctl net.ipv4.conf.all.forwarding', '\$', 10)
			result = re.search('net.ipv4.conf.all.forwarding = 1', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
			# Check if iptables forwarding is accepted
			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
			mySSH.close()
		if svrName == 'ofqot':
			mySSH.open(ipAddr, userName, password)
			# Check if X2 route to nepes enb/epc exists
1524
			mySSH.command('ip route | grep --colour=never "192.168.68.128/26"', '\$', 10)
1525
			result = re.search('172.21.16.137', mySSH.getBefore())
1526
			if result is None:
1527
				mySSH.command('echo ' + password + ' | sudo -S ip route add 192.168.68.128/26 via 172.21.16.137 dev enp2s0', '\$', 10)
1528 1529 1530 1531 1532 1533
			# Check if forwarding is enabled
			mySSH.command('sysctl net.ipv4.conf.all.forwarding', '\$', 10)
			result = re.search('net.ipv4.conf.all.forwarding = 1', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S sysctl net.ipv4.conf.all.forwarding=1', '\$', 10)
			# Check if iptables forwarding is accepted
1534
			mySSH.command('echo ' + password + ' | sudo -S iptables -L FORWARD', '\$', 10)
1535 1536 1537 1538
			result = re.search('Chain FORWARD .*policy ACCEPT', mySSH.getBefore())
			if result is None:
				mySSH.command('echo ' + password + ' | sudo -S iptables -P FORWARD ACCEPT', '\$', 10)
			mySSH.close()