Commit f57e1be8 authored by Raphael Defosseux's avatar Raphael Defosseux

CI: first draft of standalone SPGW-U CI

  -- Proper pipeline
  -- proper HTML report generator
Signed-off-by: default avatarRaphael Defosseux <raphael.defosseux@openairinterface.org>
parent 26489b0b
......@@ -101,98 +101,46 @@ pipeline {
steps {
script {
sh "git clean -x -d -f > /dev/null 2>&1"
sh "tar -cjhf /tmp/openair-cn-cups.tar.bz2 ."
sh "mv /tmp/openair-cn-cups.tar.bz2 ."
copyTo2ndServer('openair-cn-cups.tar.bz2', new_host_flag, new_host_user, new_host)
sh "tar -cjhf /tmp/openair-spgwu.tar.bz2 ."
sh "mv /tmp/openair-spgwu.tar.bz2 ."
copyTo2ndServer('openair-spgwu.tar.bz2', new_host_flag, new_host_user, new_host)
sh "mkdir -p archives"
if (new_host_flag) {
sh "mkdir -p archives/oai-spgwc-cfg"
sh "mkdir -p archives/oai-spgwu-cfg"
}
}
}
}
stage ('Start OAI SPGW-C Container') {
steps {
script {
myShCmd('docker run --privileged --name ci-oai-spgwc -d ubuntu:bionic /bin/bash -c "sleep infinity"', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./openair-cn-cups.tar.bz2 ci-oai-spgwc:/home', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "cd /home && tar -xjf openair-cn-cups.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "rm -f /home/openair-cn-cups.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ~/.gitconfig ci-oai-spgwc:/root', new_host_flag, new_host_user, new_host)
}
}
post {
success {
sh "echo 'OAI-SPGW-C START: OK' > archives/spgwc-docker-start.log"
}
unsuccessful {
sh "echo 'OAI-SPGW-C START: KO' > archives/spgwc-docker-start.log"
}
}
}
stage ('Start OAI SPGW-U Container') {
steps {
script {
myShCmd('docker run --privileged --name ci-oai-spgwu -d ubuntu:bionic /bin/bash -c "sleep infinity"', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./openair-cn-cups.tar.bz2 ci-oai-spgwu:/home', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "cd /home && tar -xjf openair-cn-cups.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "rm -f /home/openair-cn-cups.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ~/.gitconfig ci-oai-spgwu:/root', new_host_flag, new_host_user, new_host)
}
}
post {
success {
sh "echo 'OAI-SPGW-U START: OK' > archives/spgwu-docker-start.log"
}
unsuccessful {
sh "echo 'OAI-SPGW-U START: KO' > archives/spgwu-docker-start.log"
}
}
}
stage('Install libs & sw') {
stage('Build Core Network Function') {
parallel {
stage ('SPGW-C SW') {
stage ('Build SPGW-U Image') {
steps {
script {
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "apt-get update && apt-get upgrade --yes" > archives/spgwc_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "apt-get install --yes git psmisc net-tools" >> archives/spgwc_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "cd /home/build/scripts && ./build_spgwc --install-deps --force" >> archives/spgwc_install.log', new_host_flag, new_host_user, new_host)
}
}
post {
always {
script {
copyFrom2ndServer('archives/spgwc_install.log', 'archives', new_host_flag, new_host_user, new_host)
if (env.ghprbPullId != null) {
// Building a temporary image
myShCmd('docker image prune --force', new_host_flag, new_host_user, new_host)
myShCmd('docker build --target oai-spgwu-tiny --tag oai-spgwu-tiny:ci-temp --file ci-scripts/Dockerfile.ubuntu18.04 --build-arg EURECOM_PROXY="http://proxy.eurecom.fr:8080" --build-arg BUILD_FOR_CI="True" --build-arg CI_SRC_BRANCH="' + env.ghprbSourceBranch + '" --build-arg CI_SRC_COMMIT="' + env.ghprbActualCommit + '" --build-arg CI_DEST_BRANCH="develop" . > archives/spgwu_docker_image_build.log 2>&1', new_host_flag, new_host_user, new_host)
} else {
// Currently this pipeline only runs for pushes to `develop` branch
// First clean image registry
myShCmd('docker image rm oai-spgwu-tiny:develop', new_host_flag, new_host_user, new_host)
myShCmd('docker image prune --force', new_host_flag, new_host_user, new_host)
myShCmd('docker build --target oai-spgwu-tiny --tag oai-spgwu-tiny:develop --file ci-scripts/Dockerfile.ubuntu18.04 --build-arg EURECOM_PROXY="http://proxy.eurecom.fr:8080" --build-arg CI_SRC_BRANCH="develop" . > archives/spgwu_docker_image_build.log 2>&1', new_host_flag, new_host_user, new_host)
}
}
success {
sh "echo 'OAI-SPGW-C SW INSTALL: OK' >> archives/spgwc_install.log"
}
unsuccessful {
sh "echo 'OAI-SPGW-C SW INSTALL: KO' >> archives/spgwc_install.log"
}
}
}
stage ('SPGW-U SW') {
steps {
script {
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "apt-get update && apt-get upgrade --yes" > archives/spgwu_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "apt-get install --yes git psmisc net-tools iproute2" >> archives/spgwu_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "cd /home/build/scripts && ./build_spgwu --install-deps --force" >> archives/spgwu_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker image ls >> archives/spgwu_docker_image_build.log', new_host_flag, new_host_user, new_host)
}
}
post {
always {
script {
copyFrom2ndServer('archives/spgwu_install.log', 'archives', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/spgwu_docker_image_build.log', 'archives', new_host_flag, new_host_user, new_host)
}
}
success {
sh "echo 'OAI-SPGW-U SW INSTALL: OK' >> archives/spgwu_install.log"
sh "echo 'OAI-SPGW-U DOCKER IMAGE BUILD: OK' >> archives/spgwu_docker_image_build.log"
}
unsuccessful {
sh "echo 'OAI-SPGW-U SW INSTALL: KO' >> archives/spgwu_install.log"
sh "echo 'OAI-SPGW-U DOCKER IMAGE BUILD: KO' >> archives/spgwu_docker_image_build.log"
}
}
}
......@@ -205,9 +153,9 @@ pipeline {
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "apt-get update && apt-get upgrade --yes" > archives/cppcheck_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "apt-get install --yes cppcheck bzip2" >> archives/cppcheck_install.log', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./openair-cn-cups.tar.bz2 ci-cn-cppcheck:/home', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "cd /home && tar -xjf openair-cn-cups.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "rm -f /home/openair-cn-cups.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./openair-spgwu.tar.bz2 ci-cn-cppcheck:/home', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "cd /home && tar -xjf openair-spgwu.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "rm -f /home/openair-spgwu.tar.bz2"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-cn-cppcheck /bin/bash -c "cd /home && cppcheck --enable=warning --force --xml --xml-version=2 src 2> cppcheck.xml 1> cppcheck_build.log"', new_host_flag, new_host_user, new_host)
}
......@@ -232,84 +180,36 @@ pipeline {
}
}
}
stage('Build Core Network Functions') {
parallel {
stage ('SPGW-C Build') {
stage('Deploy Full EPC') {
stages {
stage('Create Docker Networks') {
steps {
script {
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "cd /home/build/scripts && ./build_spgwc --clean --build-type Release --jobs" > archives/spgwc_build.log', new_host_flag, new_host_user, new_host)
}
}
post {
always {
script {
myShCmd('docker cp ci-oai-spgwc:/home/build/log/spgwc.txt archives/spgwc_compile.log', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/spgwc_build.log', 'archives', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/spgwc_compile.log', 'archives', new_host_flag, new_host_user, new_host)
}
}
success {
sh "echo 'OAI-SPGW-C BUILD: OK' >> archives/spgwc_build.log"
}
unsuccessful {
sh "echo 'OAI-SPGW-C BUILD: KO' >> archives/spgwc_build.log"
myShCmd('docker network create --attachable --subnet 192.168.28.0/24 --ip-range 192.168.28.0/24 ci-s11', new_host_flag, new_host_user, new_host)
myShCmd('docker network create --attachable --subnet 192.168.29.0/24 --ip-range 192.168.29.0/24 ci-sx', new_host_flag, new_host_user, new_host)
myShCmd('docker network create --attachable --subnet 192.168.30.0/24 --ip-range 192.168.30.0/24 ci-s1u', new_host_flag, new_host_user, new_host)
}
}
}
stage ('SPGW-U Build') {
steps {
script {
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "cd /home/build/scripts && ./build_spgwu --clean --build-type Release --jobs" > archives/spgwu_build.log', new_host_flag, new_host_user, new_host)
}
}
post {
always {
script {
myShCmd('docker cp ci-oai-spgwu:/home/build/log/spgwu.txt archives/spgwu_compile.log', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/spgwu_build.log', 'archives', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/spgwu_compile.log', 'archives', new_host_flag, new_host_user, new_host)
}
}
success {
sh "echo 'OAI-SPGW-U BUILD: OK' >> archives/spgwu_build.log"
}
unsuccessful {
sh "echo 'OAI-SPGW-U BUILD: KO' >> archives/spgwu_build.log"
}
}
}
}
}
stage ('Create Docker Networks') {
steps {
script {
myShCmd('docker network create --attachable --subnet 192.168.28.0/24 --ip-range 192.168.28.0/24 ci-s11', new_host_flag, new_host_user, new_host)
myShCmd('docker network create --attachable --subnet 192.168.29.0/24 --ip-range 192.168.29.0/24 ci-sx', new_host_flag, new_host_user, new_host)
myShCmd('docker network create --attachable --subnet 192.168.30.0/24 --ip-range 192.168.30.0/24 ci-s1u', new_host_flag, new_host_user, new_host)
}
}
}
stage('Config Core Network Functions') {
parallel {
stage ('SPGW-C Config') {
stage('Deploy OAI-SPGW-C') {
steps {
script {
myShCmd('docker run --privileged --name ci-oai-spgwc -d oai-spgwc:develop /bin/bash -c "sleep infinity"', new_host_flag, new_host_user, new_host)
// First connect S11 --> will be eth1
myShCmd('docker network connect ci-s11 ci-oai-spgwc', new_host_flag, new_host_user, new_host)
// Then connect SX --> will be eth2
myShCmd('docker network connect ci-sx ci-oai-spgwc', new_host_flag, new_host_user, new_host)
sh "python3 ci-scripts/generateConfigFiles.py --kind=SPGW-C --s11c=eth1 --sxc=eth2"
sh "wget --quiet https://raw.githubusercontent.com/OPENAIRINTERFACE/openair-spgwc/develop/ci-scripts/generateConfigFiles.py -O ci-scripts/generateSpgwcConfigFiles.py"
sh "python3 ci-scripts/generateSpgwcConfigFiles.py --kind=SPGW-C --s11c=eth1 --sxc=eth2 --from_docker_file"
copyTo2ndServer('spgwc-cfg.sh', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./spgwc-cfg.sh ci-oai-spgwc:/home', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "cd /home && chmod 777 spgwc-cfg.sh && ./spgwc-cfg.sh" >> archives/spgwc_config.log', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./spgwc-cfg.sh ci-oai-spgwc:/openair-spgwc', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "cd /openair-spgwc && chmod 777 spgwc-cfg.sh && ./spgwc-cfg.sh" >> archives/spgwc_config.log', new_host_flag, new_host_user, new_host)
}
}
post {
always {
script {
myShCmd('docker cp ci-oai-spgwc:/usr/local/etc/oai/. archives/oai-spgwc-cfg', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/spgwc_config.log', 'archives', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/oai-spgwc-cfg/*.*', 'archives/oai-spgwc-cfg', new_host_flag, new_host_user, new_host)
}
}
success {
......@@ -320,20 +220,25 @@ pipeline {
}
}
}
stage ('SPGW-U Config') {
stage('Deploy OAI-SPGWU-TINY') {
steps {
script {
sh "sleep 10"
if (env.ghprbPullId != null) {
myShCmd('docker run --privileged --name ci-oai-spgwu -d oai-spgwu:ci-temp /bin/bash -c "sleep infinity"', new_host_flag, new_host_user, new_host)
} else {
myShCmd('docker run --privileged --name ci-oai-spgwu -d oai-spgwu:develop /bin/bash -c "sleep infinity"', new_host_flag, new_host_user, new_host)
}
// First connect S1-U --> will be eth1
myShCmd('docker network connect ci-s1u ci-oai-spgwu', new_host_flag, new_host_user, new_host)
// Then connect SX --> will be eth2
myShCmd('docker network connect ci-sx ci-oai-spgwu', new_host_flag, new_host_user, new_host)
// Retrieve IP address on SPGW-C instance.. Reason for the sleep 10 above.. making sure it's done
OAI_SPGWC0_IP = myShRetCmd('docker exec -it ci-oai-spgwc /bin/bash -c "ifconfig eth2 | egrep inet" | sed -e "s@^.*inet @@" -e "s@ netmask.*@@"', new_host_flag, new_host_user, new_host)
sh "python3 ci-scripts/generateConfigFiles.py --kind=SPGW-U --sxc_ip_addr=${OAI_SPGWC0_IP} --sxu=eth2 --s1u=eth1"
// To be replaced w/ wget from spgwu repo.
sh "python3 ci-scripts/generateConfigFiles.py --kind=SPGW-U --sxc_ip_addr=${OAI_SPGWC0_IP} --sxu=eth2 --s1u=eth1 --from_docker_file"
copyTo2ndServer('spgwu-cfg.sh', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./spgwu-cfg.sh ci-oai-spgwu:/home', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "cd /home && chmod 777 spgwu-cfg.sh && ./spgwu-cfg.sh" >> archives/spgwu_config.log', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ./spgwu-cfg.sh ci-oai-spgwu:/openair-spgwu-tiny', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "cd /openair-spgwu-tiny && chmod 777 spgwu-cfg.sh && ./spgwu-cfg.sh" >> archives/spgwu_config.log', new_host_flag, new_host_user, new_host)
}
}
post {
......@@ -357,18 +262,18 @@ pipeline {
stage ('Start-Check-Stop OAI cNFs') {
steps {
script {
myShCmd('docker exec -d -w /home ci-oai-spgwc /bin/bash -c "nohup spgwc -o -c /usr/local/etc/oai/spgw_c.conf > spgwc_check_run.log 2>&1"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -d ci-oai-spgwc /bin/bash -c "nohup ./bin/oai_spgwc -o -c ./etc/spgw_c.conf > spgwc_check_run.log 2>&1"', new_host_flag, new_host_user, new_host)
sh "sleep 2"
myShCmd('docker exec -d -w /home ci-oai-spgwu /bin/bash -c "nohup spgwu -o -c /usr/local/etc/oai/spgw_u.conf > spgwu_check_run.log 2>&1"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -d ci-oai-spgwu /bin/bash -c "nohup ./bin/oai_spgwu -o -c ./etc/spgw_u.conf > spgwu_check_run.log 2>&1"', new_host_flag, new_host_user, new_host)
sh "sleep 20"
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "killall spgwc"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "killall spgwu"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwc /bin/bash -c "killall oai_spgwc"', new_host_flag, new_host_user, new_host)
myShCmd('docker exec -it ci-oai-spgwu /bin/bash -c "killall oai_spgwu"', new_host_flag, new_host_user, new_host)
}
}
post {
always {
myShCmd('docker cp ci-oai-spgwc:/home/spgwc_check_run.log archives', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ci-oai-spgwu:/home/spgwu_check_run.log archives', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ci-oai-spgwc:/openair-spgwc/spgwc_check_run.log archives', new_host_flag, new_host_user, new_host)
myShCmd('docker cp ci-oai-spgwu:/openair-spgwu-tiny/spgwu_check_run.log archives', new_host_flag, new_host_user, new_host)
copyFrom2ndServer('archives/*_check_run.log', 'archives', new_host_flag, new_host_user, new_host)
// add a check python script
}
......@@ -419,11 +324,33 @@ pipeline {
cleanup {
script {
// Killing all containers
myShCmd('docker rm -f ci-oai-spgwc ci-oai-spgwu', new_host_flag, new_host_user, new_host)
// Killing all networks
myShCmd('docker network prune --force', new_host_flag, new_host_user, new_host)
// Later on maybe we will restart docker?
//myShCmd('sudo service docker restart', new_host_flag, new_host_user, new_host)
try {
myShCmd('docker rm -f ci-oai-spgwc ci-oai-spgwu', new_host_flag, new_host_user, new_host)
} catch (Exception e) {
echo "No problem: we may not started all containers"
}
// Removing the networks
try {
myShCmd('docker network rm ci-s11 ci-sx ci-s1u', new_host_flag, new_host_user, new_host)
} catch (Exception e) {
echo "No problem: we may not started all docker networks"
}
// In case of build failure, we might have temporary containers still running. TBD!
// Removing temporary / intermediate images
try {
if (env.ghprbPullId != null) {
myShCmd('docker image rm --force oai-spgwu:ci-temp', new_host_flag, new_host_user, new_host)
}
} catch (Exception e) {
echo "We failed to delete the OAI-SPGWU temp image"
}
try {
myShCmd('docker image prune --force', new_host_flag, new_host_user, new_host)
} catch (Exception e) {
echo "We failed to prune all unneeded intermediate images"
}
// Zipping all archived log files
sh "zip -r -qq docker_logs.zip archives"
if (fileExists('docker_logs.zip')) {
......@@ -432,14 +359,14 @@ pipeline {
// Generating the HTML report
if (env.ghprbPullId != null) {
sh "python3 ci-scripts/generateHtmlReport.py --job_name=${JOB_NAME} --job_id=${BUILD_ID} --job_url=${BUILD_URL} --git_url=${GIT_URL} --git_src_branch=${env.ghprbSourceBranch} --git_src_commit=${env.ghprbActualCommit} --git_pull_request=True --git_target_branch=${env.ghprbTargetBranch} --git_target_commit=${GIT_COMMIT}"
sh "sed -i -e 's#TEMPLATE_PULL_REQUEST_LINK#${env.ghprbPullLink}#g' test_results_oai_cn.html"
sh "sed -i -e 's#TEMPLATE_PULL_REQUEST_TEMPLATE#${env.ghprbPullTitle}#' test_results_oai_cn.html"
sh "sed -i -e 's#TEMPLATE_PULL_REQUEST_LINK#${env.ghprbPullLink}#g' test_results_oai_spgwu.html"
sh "sed -i -e 's#TEMPLATE_PULL_REQUEST_TEMPLATE#${env.ghprbPullTitle}#' test_results_oai_spgwu.html"
} else {
sh "python3 ci-scripts/generateHtmlReport.py --job_name=${JOB_NAME} --job_id=${BUILD_ID} --job_url=${BUILD_URL} --git_url=${GIT_URL} --git_src_branch=${GIT_BRANCH} --git_src_commit=${GIT_COMMIT}"
}
sh "sed -i -e 's#TEMPLATE_TIME#${JOB_TIMESTAMP}#' test_results_oai_cn.html"
if (fileExists('test_results_oai_cn.html')) {
archiveArtifacts artifacts: 'test_results_oai_cn.html'
sh "sed -i -e 's#TEMPLATE_TIME#${JOB_TIMESTAMP}#' test_results_oai_spgwu.html"
if (fileExists('test_results_oai_spgwu.html')) {
archiveArtifacts artifacts: 'test_results_oai_spgwu.html'
}
// Sending an email to the last committer. Maybe not the Pull Request's author.
def emailTo = getCommitterEmail()
......@@ -517,29 +444,28 @@ def updateGithubCommitStatus(build, jobMessage) {
def copyTo2ndServer(filename, flag, user, host) {
if (flag) {
if ("openair-cn-cups.tar.bz2".equals(filename)) {
sh "ssh ${user}@${host} 'rm -rf /tmp/CI-CN-CUPS'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-CUPS'"
if ("openair-spgwu.tar.bz2".equals(filename)) {
sh "ssh ${user}@${host} 'rm -rf /tmp/CI-CN-SPGWU'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-SPGWU'"
}
sh "scp ${filename} ${user}@${host}:/tmp/CI-CN-CUPS"
if ("openair-cn-cups.tar.bz2".equals(filename)) {
sh "ssh ${user}@${host} 'cd /tmp/CI-CN-CUPS && tar -xjf ${filename}'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-CUPS/archives'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-CUPS/archives/oai-spgwc-cfg'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-CUPS/archives/oai-spgwu-cfg'"
sh "scp ${filename} ${user}@${host}:/tmp/CI-CN-SPGWU"
if ("openair-spgwu.tar.bz2".equals(filename)) {
sh "ssh ${user}@${host} 'cd /tmp/CI-CN-SPGWU && tar -xjf ${filename}'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-SPGWU/archives'"
sh "ssh ${user}@${host} 'mkdir -p /tmp/CI-CN-SPGWU/archives/oai-spgwu-cfg'"
}
}
}
def copyFrom2ndServer(filename, target, flag, user, host) {
if (flag) {
sh "scp ${user}@${host}:/tmp/CI-CN-CUPS/${filename} ${target}"
sh "scp ${user}@${host}:/tmp/CI-CN-SPGWU/${filename} ${target}"
}
}
def myShCmd(cmd, flag, user, host) {
if (flag) {
sh "ssh -t -t ${user}@${host} 'cd /tmp/CI-CN-CUPS && ${cmd}'"
sh "ssh -t -t ${user}@${host} 'cd /tmp/CI-CN-SPGWU && ${cmd}'"
} else {
sh "${cmd}"
}
......
......@@ -41,19 +41,25 @@ class HtmlReport():
def generate(self):
cwd = os.getcwd()
self.file = open(cwd + '/test_results_oai_cn.html', 'w')
self.file = open(cwd + '/test_results_oai_spgwu.html', 'w')
self.generateHeader()
self.analyze_sca_log()
self.buildSummaryHeader()
self.containerStartRow()
self.initialGitSetup()
self.installLibsPackagesRow()
self.buildCompileRows()
self.configurationRow()
self.startStopCheckRow()
self.copyToTargetImage()
self.copyConfToolsToTargetImage()
self.imageSizeRow()
self.buildSummaryFooter()
self.sanityCheckSummaryHeader()
self.containerStartRow()
self.startStopCheckRow()
self.sanityCheckSummaryFooter()
self.testSummaryHeader()
self.testSummaryFooter()
......@@ -300,48 +306,58 @@ class HtmlReport():
self.file.write(' </div>\n')
def buildSummaryHeader(self):
self.file.write(' <h2>Build Summary</h2>\n')
self.file.write(' <h2>Docker Image Build Summary</h2>\n')
self.file.write(' <table class="table-bordered" width = "100%" align = "center" border = "1">\n')
self.file.write(' <tr bgcolor="#33CCFF" >\n')
self.file.write(' <th>Stage Name</th>\n')
self.file.write(' <th>OAI SPGW-C cNF</th>\n')
self.file.write(' <th>OAI SPGW-U cNF</th>\n')
self.file.write(' <th>Image Kind</th>\n')
self.file.write(' <th>OAI SPGW-U-TINY cNF</th>\n')
self.file.write(' </tr>\n')
def buildSummaryFooter(self):
self.file.write(' </table>\n')
self.file.write(' <br>\n')
def containerStartRow(self):
def initialGitSetup(self):
self.file.write(' <tr>\n')
self.file.write(' <td bgcolor="lightcyan" >Starting Docker Containers</td>\n')
self.analyze_docker_start_log('SPGW-C')
self.analyze_docker_start_log('SPGW-U')
self.file.write(' <td bgcolor="lightcyan" >Initial Git Setup</td>\n')
self.analyze_docker_build_git_part('SPGW-U')
self.file.write(' </tr>\n')
def analyze_docker_start_log(self, nfType):
logFileName = nfType.lower().replace('-','') + '-docker-start.log'
pattern = 'OAI-' + nfType.upper() + ' START:'
def analyze_docker_build_git_part(self, nfType):
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Builder Image</td>\n')
cwd = os.getcwd()
if os.path.isfile(cwd + '/archives/' + logFileName):
status = False
section_start_pattern = 'git config --global http'
section_end_pattern = 'WORKDIR /openair-spgwu-tiny/build/scripts'
section_status = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search(pattern, line)
result = re.search(section_start_pattern, line)
if result is not None:
result = re.search('OK', line)
if result is not None:
status = True
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
section_status = False
status = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK: ci-oai-' + nfType.lower() + ':\n'
cell_msg += ' -- started successfully</b></pre></td>\n'
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK:\n'
cell_msg += ' -- All Git Operations went successfully</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: ci-oai-' + nfType.lower() + ':\n'
cell_msg += ' -- did not start properly?</b></pre></td>\n'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO::\n'
cell_msg += ' -- Some Git Operations went WRONG</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
......@@ -351,37 +367,83 @@ class HtmlReport():
def installLibsPackagesRow(self):
self.file.write(' <tr>\n')
self.file.write(' <td bgcolor="lightcyan" >SW libs and packages Installation</td>\n')
self.analyze_install_log('SPGW-C')
self.analyze_install_log('SPGW-U')
self.file.write(' </tr>\n')
def analyze_install_log(self, nfType):
logFileName = nfType.lower().replace('-','') + '_install.log'
pattern = 'OAI-' + nfType.upper() + ' SW INSTALL:'
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Builder Image</td>\n')
cwd = os.getcwd()
if os.path.isfile(cwd + '/archives/' + logFileName):
status = False
section_start_pattern = 'build_spgwu --install-deps --force'
section_end_pattern = 'build_spgwu --clean --build-type Release --jobs'
section_status = False
package_install = False
folly_build_start = False
folly_build = False
spdlog_build_start = False
spdlog_build = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search(pattern, line)
result = re.search(section_start_pattern, line)
if result is not None:
result = re.search('OK', line)
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
section_status = False
if section_status:
result = re.search('SPGW-U deps installation successful', line)
if result is not None:
status = True
result = re.search('/tmp /openair-spgwu-tiny/build/scripts', line)
if result is not None:
package_install = True
result = re.search('Cloning into \'folly\'', line)
if result is not None:
folly_build_start = True
if folly_build_start:
result = re.search('Installing: /usr/local/lib/libfollybenchmark', line)
if result is not None:
folly_build_start = False
folly_build = True
result = re.search('Install spdlog from ', line)
if result is not None:
spdlog_build_start = True
if spdlog_build_start:
result = re.search('/openair-spgwu-tiny/build/scripts', line)
if result is not None:
spdlog_build_start = False
spdlog_build = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK:\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO:\n'
if nfType == 'SPGW-C':
cell_msg += ' -- build_spgwc --install-deps --force</b></pre></td>\n'
cell_msg += ' -- build_spgwu --install-deps --force\n'
if package_install:
cell_msg += ' ** Packages Installation: OK\n'
else:
cell_msg += ' ** Packages Installation: KO\n'
if folly_build:
cell_msg += ' ** Folly Installation: OK\n'
else:
cell_msg += ' ** Folly Installation: KO\n'
if spdlog_build:
cell_msg += ' ** spdlog Installation: OK\n'
else:
cell_msg += ' -- build_spgwu --install-deps --force</b></pre></td>\n'
cell_msg += ' ** spdlog Installation: KO\n'
cell_msg += '</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
......@@ -389,85 +451,263 @@ class HtmlReport():
def buildCompileRows(self):
self.file.write(' <tr>\n')
self.file.write(' <td rowspan=2 bgcolor="lightcyan" >cNF Compile / Build</td>\n')
self.analyze_build_log('SPGW-C')
self.analyze_build_log('SPGW-U')
self.file.write(' </tr>\n')
self.file.write(' <tr>\n')
self.analyze_compile_log('SPGW-C')
self.analyze_compile_log('SPGW-U')
self.file.write(' </tr>\n')
def analyze_build_log(self, nfType):
logFileName = nfType.lower().replace('-','') + '_build.log'
pattern = 'OAI-' + nfType.upper() + ' BUILD:'
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Builder Image</td>\n')
cwd = os.getcwd()
if os.path.isfile(cwd + '/archives/' + logFileName):
status = False
section_start_pattern = 'build_spgwu --clean --build-type Release --jobs'
section_end_pattern = 'cat /openair-spgwu-tiny/build/log/spgwu.txt'
section_status = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search(pattern, line)
result = re.search(section_start_pattern, line)
if result is not None:
result = re.search('OK', line)
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
section_status = False
if section_status:
result = re.search('spgwu installed', line)
if result is not None:
status = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK:\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO:\n'
if nfType == 'SPGW-C':
cell_msg += ' -- build_spgwc --clean --build-type Release --jobs</b></pre></td>\n'
else:
cell_msg += ' -- build_spgwu --clean --build-type Release --jobs</b></pre></td>\n'
cell_msg += ' -- build_spgwu --clean --build-type Release --jobs</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
def analyze_compile_log(self, nfType):
logFileName = nfType.lower().replace('-','') + '_compile.log'
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Builder Image</td>\n')
cwd = os.getcwd()
nb_errors = 0
nb_warnings = 0
if os.path.isfile(cwd + '/archives/' + logFileName):
section_start_pattern = 'cat /openair-spgwu-tiny/build/log/spgwu.txt'
section_end_pattern = 'FROM ubuntu:bionic as oai-spgwu-tiny$'
section_status = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search('error:', line)
result = re.search(section_start_pattern, line)
if result is not None:
nb_errors += 1
result = re.search('warning:', line)
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
nb_warnings += 1
section_status = False
if section_status:
result = re.search('error:', line)
if result is not None:
nb_errors += 1
result = re.search('warning:', line)
if result is not None:
nb_warnings += 1
logfile.close()
if nb_warnings == 0 and nb_errors == 0:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
elif nb_warnings < 20 and nb_errors == 0:
cell_msg = ' <td bgcolor="Orange"><pre style="border:none; background-color:Orange"><b>'
cell_msg = ' <td bgcolor="Orange"><pre style="border:none; background-color:Orange"><b>'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
if nb_errors > 0:
cell_msg += str(nb_errors) + ' errors found in compile log\n'
cell_msg += str(nb_warnings) + ' warnings found in compile log</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
def copyToTargetImage(self):
self.file.write(' <tr>\n')
self.file.write(' <td bgcolor="lightcyan" >SW libs Installation / Copy from Builder</td>\n')
self.analyze_copy_log('SPGW-U')
self.file.write(' </tr>\n')
def analyze_copy_log(self, nfType):
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Target Image</td>\n')
cwd = os.getcwd()
if os.path.isfile(cwd + '/archives/' + logFileName):
section_start_pattern = 'FROM ubuntu:bionic as oai-spgwu-tiny$'
section_end_pattern = 'WORKDIR /openair-spgwu-tiny/etc'
section_status = False
status = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search(section_start_pattern, line)
if result is not None:
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
section_status = False
status = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK:\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO:\n'
cell_msg += '</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
def configurationRow(self):
def copyConfToolsToTargetImage(self):
self.file.write(' <tr>\n')
self.file.write(' <td bgcolor="lightcyan" >cNF Configuration</td>\n')
self.analyze_config_log('SPGW-C')
self.analyze_config_log('SPGW-U')
self.file.write(' <td bgcolor="lightcyan" >Copy Template Conf / Tools from Builder</td>\n')
self.analyze_copy_conf_tool_log('SPGW-U')
self.file.write(' </tr>\n')
def analyze_config_log(self, nfType):
def analyze_copy_conf_tool_log(self, nfType):
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Target Image</td>\n')
cwd = os.getcwd()
if os.path.isfile(cwd + '/archives/' + logFileName):
section_start_pattern = 'WORKDIR /openair-spgwu-tiny/etc'
section_end_pattern = 'Successfully tagged oai-spgwu-tiny'
section_status = False
status = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search(section_start_pattern, line)
if result is not None:
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
section_status = False
status = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK:\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO:\n'
cell_msg += '</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
def imageSizeRow(self):
self.file.write(' <tr>\n')
self.file.write(' <td bgcolor="lightcyan" >Image Size</td>\n')
self.analyze_image_size_log('SPGW-U')
self.file.write(' </tr>\n')
def analyze_image_size_log(self, nfType):
if nfType != 'SPGW-U':
self.file.write(' <td>N/A</td>\n')
self.file.write(' <td>Wrong NF Type for this Report</td>\n')
return
logFileName = 'spgwu_docker_image_build.log'
self.file.write(' <td>Target Image</td>\n')
cwd = os.getcwd()
if os.path.isfile(cwd + '/archives/' + logFileName):
section_start_pattern = 'Successfully tagged oai-spgwu-tiny'
section_end_pattern = 'OAI-SPGW-U DOCKER IMAGE BUILD'
section_status = False
status = False
with open(cwd + '/archives/' + logFileName, 'r') as logfile:
for line in logfile:
result = re.search(section_start_pattern, line)
if result is not None:
section_status = True
result = re.search(section_end_pattern, line)
if result is not None:
section_status = False
if section_status:
if self.git_pull_request:
result = re.search('oai-spgwu-tiny *ci-temp', line)
else:
result = re.search('oai-spgwu-tiny *develop', line)
if result is not None:
result = re.search('ago *([0-9A-Z]+)', line)
if result is not None:
size = result.group(1)
status = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK: ' + size + '\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO:\n'
cell_msg += '</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
def sanityCheckSummaryHeader(self):
self.file.write(' <h2>Sanity Check Deployment Summary</h2>\n')
self.file.write(' <table class="table-bordered" width = "100%" align = "center" border = "1">\n')
self.file.write(' <tr bgcolor="#33CCFF" >\n')
self.file.write(' <th>Stage Name</th>\n')
self.file.write(' <th>OAI SPGW-C cNF</th>\n')
self.file.write(' <th>OAI SPGW-U cNF</th>\n')
self.file.write(' </tr>\n')
def sanityCheckSummaryFooter(self):
self.file.write(' </table>\n')
self.file.write(' <br>\n')
def containerStartRow(self):
self.file.write(' <tr>\n')
self.file.write(' <td bgcolor="lightcyan" >Starting/Configuring Docker Containers</td>\n')
self.analyze_docker_start_log('SPGW-C')
self.analyze_docker_start_log('SPGW-U')
self.file.write(' </tr>\n')
def analyze_docker_start_log(self, nfType):
logFileName = nfType.lower().replace('-','') + '_config.log'
pattern = 'OAI-' + nfType.upper() + ' CONFIG:'
......@@ -482,15 +722,18 @@ class HtmlReport():
if result is not None:
status = True
logfile.close()
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK:\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO:\n'
cell_msg += '</b></pre></td>\n'
if status:
cell_msg = ' <td bgcolor="LimeGreen"><pre style="border:none; background-color:LimeGreen"><b>'
cell_msg += 'OK: ci-oai-' + nfType.lower().replace('-','') + ':\n'
cell_msg += ' -- started successfully\n'
cell_msg += ' -- was configured successfully\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: ci-oai-' + nfType.lower().replace('-','') + ':\n'
cell_msg += ' -- did not start properly?\n'
cell_msg += '</b></pre></td>\n'
else:
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg = ' <td bgcolor="Tomato"><pre style="border:none; background-color:Tomato"><b>'
cell_msg += 'KO: logfile (' + logFileName + ') not found</b></pre></td>\n'
self.file.write(cell_msg)
......@@ -561,6 +804,25 @@ class HtmlReport():
def testSummaryFooter(self):
self.file.write(' <br>\n')
def Usage():
print('----------------------------------------------------------------------------------------------------------------------')
print('generateHtmlReport.py')
print(' Generate an HTML report for the Jenkins pipeline on openair-spgwu-tiny.')
print('----------------------------------------------------------------------------------------------------------------------')
print('Usage: python3 generateHtmlReport.py [options]')
print(' --help Show this help.')
print('---------------------------------------------------------------------------------------------- Mandatory Options -----')
print(' --job_name=[Jenkins Job name]')
print(' --job_id=[Jenkins Job Build ID]')
print(' --job_url=[Jenkins Job Build URL]')
print(' --git_url=[Git Repository URL]')
print(' --git_src_branch=[Git Source Branch Name]')
print(' --git_src_commit=[Git Source Commit SHA-ONE]')
print('----------------------------------------------------------------------------------------------- Optional Options -----')
print(' --git_pull_request=True')
print(' --git_target_branch=[Git Target Branch Name]')
print(' --git_target_commit=[Git Target Commit SHA-ONE]')
#--------------------------------------------------------------------------------------------------------
#
# Start of main
......@@ -575,6 +837,7 @@ HTML = HtmlReport()
while len(argvs) > 1:
myArgv = argvs.pop(1)
if re.match('^\-\-help$', myArgv, re.IGNORECASE):
Usage()
sys.exit(0)
elif re.match('^\-\-job_name=(.+)$', myArgv, re.IGNORECASE):
matchReg = re.match('^\-\-job_name=(.+)$', myArgv, re.IGNORECASE)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment