Add helper class for core network handling in CI
This adds a helper class to manage various core networks in the CI testing framework. Specifically, it reads from a YAML file information such as a node, (un-)deployment script (file), log collection instructions, etc., in order to allow a common interface to be used to deploy and undeploy various core network implementations. This commit adds the class (in cls_corenetwork.py) as well as tests for this class.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
# * 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
|
||||
# */
|
||||
#---------------------------------------------------------------------
|
||||
#
|
||||
# Required Python Version
|
||||
# Python 3.x
|
||||
#
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import yaml
|
||||
import re
|
||||
|
||||
import cls_cmd
|
||||
|
||||
def listify(s):
|
||||
if s is None:
|
||||
return None
|
||||
if isinstance(s, list):
|
||||
return [str(x) for x in s]
|
||||
return [str(s)]
|
||||
|
||||
class CoreNetwork:
|
||||
|
||||
def __init__(self, cn_name, node=None, d=None, filename="ci_infra.yaml"):
|
||||
with open(filename, "r") as f:
|
||||
all_cns = yaml.load(f, Loader=yaml.FullLoader) # TODO why full?
|
||||
c = all_cns.get(cn_name)
|
||||
if c is None:
|
||||
raise Exception(f'no such core network name "{cn_name}" in "{filename}"')
|
||||
self._cn_name = cn_name
|
||||
self._host = c.get('Host').strip()
|
||||
if self._host == "%%current_host%%":
|
||||
if node is None:
|
||||
raise Exception(f"core network {self} requires node, but none provided (cannot replace %%current_host%%)")
|
||||
self._host = node
|
||||
if d is not None:
|
||||
raise Exception("directory handling not implemented")
|
||||
self._deploy = listify(c.get('Deploy'))
|
||||
self._undeploy = listify(c.get('Undeploy'))
|
||||
self._logCollect = listify(c.get('LogCollect'))
|
||||
# check if at least one command contains "%%log_dir%%"
|
||||
if self._logCollect and not any(filter(lambda s: "%%log_dir%%" in s, self._logCollect)):
|
||||
raise ValueError(f"(At least one) LogCollect expression for {cn_name} must contain \"%%log_dir%%\"")
|
||||
self._getNetwork = listify(c.get('NetworkScript'))
|
||||
self._cmd_prefix = c.get('CmdPrefix')
|
||||
iperf = c.get('RunIperf3Server')
|
||||
self._run_iperf = bool(iperf if iperf is not None else True)
|
||||
logging.info(f'initialized core {self} from {filename}')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self._cn_name}@{self._host} [IP: {self.getIP()}]"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def _exec_script(host, line, silent):
|
||||
# take off "!", split in words
|
||||
words = line[1:].strip().split(" ")
|
||||
script_name = words[0]
|
||||
options = " ".join(words[1:])
|
||||
ret = cls_cmd.runScript(host, script_name, 300, parameters=options, silent=silent)
|
||||
return ret
|
||||
|
||||
def _command(self, cmd_list, must_succeed=False, silent=False):
|
||||
succeeded = True
|
||||
output = ""
|
||||
with cls_cmd.getConnection(self._host) as c:
|
||||
for cmd in cmd_list:
|
||||
cmd = cmd.strip()
|
||||
if cmd.startswith("!"):
|
||||
ret = CoreNetwork._exec_script(self._host, cmd, silent=silent)
|
||||
else:
|
||||
ret = c.run(cmd, silent=silent)
|
||||
output += ret.stdout
|
||||
if ret.returncode != 0:
|
||||
logging.warn(f"cmd \"{cmd}\" returned code {ret.returncode}, stdout {ret.stdout}")
|
||||
if ret.returncode != 0 and must_succeed:
|
||||
succeeded = False
|
||||
break
|
||||
return succeeded, output
|
||||
|
||||
def deploy(self):
|
||||
# we first undeploy to make sure the core has been stopped
|
||||
logging.info(f'undeploy core network {self} before deployment')
|
||||
self._command(self._undeploy)
|
||||
logging.info(f'deploy core network {self}')
|
||||
success, output = self._command(self._deploy, must_succeed=True)
|
||||
if not success:
|
||||
logging.error(f'failure during deployment of core network {self}:\n{output}')
|
||||
return False, output
|
||||
logging.info(f'retrieve IP address')
|
||||
ip = self.getIP()
|
||||
if ip is None:
|
||||
logging.error(f'could not retrieve pingable address of core network {self}')
|
||||
return False, output
|
||||
logging.info(f'deployed core network {self}, pingable IP address {ip}')
|
||||
return True, output
|
||||
|
||||
def _collect_logs(self, log_dir):
|
||||
logging.info(f'collecting logs into (local) {log_dir}')
|
||||
remote_dir = "/tmp/cn-undeploy-logs"
|
||||
with cls_cmd.getConnection(self._host) as c:
|
||||
# create a directory for log collection
|
||||
c.run(f'rm -rf {remote_dir}')
|
||||
ret = c.run(f'mkdir {remote_dir}')
|
||||
if ret.returncode != 0:
|
||||
logging.error("cannot create directory for log collection")
|
||||
return []
|
||||
# inform core network implementation where to store logs
|
||||
# (remote_dir), and trigger log collection
|
||||
log_cmd = [s.replace('%%log_dir%%', remote_dir) for s in self._logCollect]
|
||||
self._command(log_cmd)
|
||||
# enumerate collected files
|
||||
ret = c.run(f'ls {remote_dir}/*')
|
||||
if ret.returncode != 0:
|
||||
logging.error("cannot enumerate log files")
|
||||
return []
|
||||
log_files = []
|
||||
# copy them to the executor one by one, and store in log_dir
|
||||
for f in ret.stdout.split("\n"):
|
||||
l = f.replace(remote_dir, log_dir)
|
||||
c.copyin(f, l)
|
||||
log_files.append(l)
|
||||
c.run(f'rm -rf {remote_dir}')
|
||||
return log_files
|
||||
|
||||
def undeploy(self, log_dir=None):
|
||||
log_files = []
|
||||
if log_dir is not None:
|
||||
log_files = self._collect_logs(log_dir)
|
||||
else:
|
||||
logging.warning("no directory for log collection specified, cannot retrieve core network logs")
|
||||
logging.info(f'undeploy core network {self}')
|
||||
_, output = self._command(self._undeploy)
|
||||
return log_files, output
|
||||
|
||||
def getIP(self):
|
||||
success, output = self._command(self._getNetwork)
|
||||
result = re.search(r'inet (?P<ip>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output)
|
||||
if success and result and result.group('ip'):
|
||||
return result.group('ip')
|
||||
return None
|
||||
|
||||
def getCmdPrefix(self):
|
||||
return self._cmd_prefix or ""
|
||||
|
||||
def getHost(self):
|
||||
return self._host
|
||||
|
||||
def runIperf3Server(self):
|
||||
return self._run_iperf
|
||||
@@ -12,6 +12,7 @@ To run individual unit tests, start them like so:
|
||||
|
||||
python tests/build.py -v
|
||||
python tests/cmd.py -v
|
||||
python tests/corenetwork.py -v
|
||||
python tests/deployment.py -v
|
||||
python tests/iperf-analysis.py -v
|
||||
python tests/ping-iperf.py -v
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
test:
|
||||
Host: localhost
|
||||
NetworkScript: echo "inet 127.0.0.1 mtu 1500"
|
||||
Deploy: echo deploy
|
||||
Undeploy: echo undeploy
|
||||
LogCollect: echo logs > %%log_dir%%/logs
|
||||
|
||||
test_list:
|
||||
Host: localhost
|
||||
NetworkScript: echo "inet 127.0.0.1 mtu 1500"
|
||||
Deploy:
|
||||
- echo deploy
|
||||
- echo multistep deploy!!
|
||||
Undeploy:
|
||||
- "echo undeploy"
|
||||
- "echo terminated"
|
||||
LogCollect:
|
||||
- echo logs > %%log_dir%%/logs
|
||||
RunIperf3Server: false
|
||||
|
||||
test_fail:
|
||||
Host: localhost
|
||||
NetworkScript: echo "inet 127.0.0.1 mtu 1500"
|
||||
Deploy:
|
||||
- echo deploy
|
||||
- "false"
|
||||
- echo multistep deploy!!
|
||||
Undeploy:
|
||||
- "echo undeploy"
|
||||
- "echo terminated"
|
||||
LogCollect:
|
||||
- echo logs > %%log_dir%%/logs
|
||||
|
||||
test_script:
|
||||
Host: localhost
|
||||
NetworkScript: echo "inet 127.0.0.1 mtu 1500"
|
||||
Deploy: ! tests/scripts/deploy-core.sh
|
||||
Undeploy: "echo undeploy"
|
||||
LogCollect: echo logs > %%log_dir%%/logs
|
||||
|
||||
test_script_fail:
|
||||
Host: localhost
|
||||
NetworkScript: echo "inet 127.0.0.1 mtu 1500"
|
||||
Deploy: ! tests/scripts/deploy-core.sh WILLFAIL
|
||||
Undeploy: "echo undeploy"
|
||||
LogCollect: echo logs > %%log_dir%%/logs
|
||||
@@ -0,0 +1,66 @@
|
||||
import sys
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
stream=sys.stdout,
|
||||
format="[%(asctime)s] %(levelname)8s: %(message)s"
|
||||
)
|
||||
import os
|
||||
|
||||
import unittest
|
||||
|
||||
sys.path.append('./') # to find OAI imports below
|
||||
import cls_corenetwork
|
||||
import cls_cmd
|
||||
|
||||
class TestCoreNetwork(unittest.TestCase):
|
||||
def test_simple_core(self):
|
||||
c = cls_corenetwork.CoreNetwork("test", filename="tests/config/test_core_infra.yaml")
|
||||
success, output = c.deploy()
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(output, "deploy")
|
||||
# should not have it set in config
|
||||
self.assertTrue(c.runIperf3Server())
|
||||
self.assertEqual(c.getIP(), "127.0.0.1")
|
||||
log_files, output = c.undeploy(log_dir="/tmp")
|
||||
self.assertEqual(output, "undeploy")
|
||||
with cls_cmd.LocalCmd() as cmd:
|
||||
# there must be one log file for this (test) core
|
||||
self.assertEqual(len(log_files), 1)
|
||||
# test core writes to %%log_dir%%/logs, which expands to /tmp/logs
|
||||
l = log_files[0]
|
||||
self.assertEqual(l, "/tmp/logs")
|
||||
ret = cmd.run(f"cat {l}")
|
||||
self.assertEqual(ret.returncode, 0) # command must succeed
|
||||
self.assertEqual(ret.stdout, "logs") # output should be "logs"
|
||||
|
||||
def test_core_list(self):
|
||||
c = cls_corenetwork.CoreNetwork("test_list", filename="tests/config/test_core_infra.yaml")
|
||||
success, _ = c.deploy()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(c.runIperf3Server())
|
||||
c.undeploy(log_dir="/tmp")
|
||||
|
||||
def test_core_fail(self):
|
||||
c = cls_corenetwork.CoreNetwork("test_fail", filename="tests/config/test_core_infra.yaml")
|
||||
success, _ = c.deploy()
|
||||
self.assertFalse(success)
|
||||
# undeployment should still work
|
||||
c.undeploy(log_dir="/tmp")
|
||||
|
||||
def test_core_script(self):
|
||||
c = cls_corenetwork.CoreNetwork("test_script", filename="tests/config/test_core_infra.yaml")
|
||||
success, output = c.deploy()
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(output, "deployment from script")
|
||||
c.undeploy()
|
||||
|
||||
def test_core_script_fail(self):
|
||||
c = cls_corenetwork.CoreNetwork("test_script_fail", filename="tests/config/test_core_infra.yaml")
|
||||
success, output = c.deploy()
|
||||
self.assertFalse(success)
|
||||
self.assertEqual(output, "deployment from script\nfailing")
|
||||
c.undeploy()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
function die() {
|
||||
echo $@
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "deployment from script"
|
||||
|
||||
[ $# -gt 0 ] && die "failing"
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user