diff --git a/.gitignore b/.gitignore index 8d3479b7..a85b41ac 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ state *.pyc selftest/trial_test/ example/resources.conf +ttcn3/resources.conf diff --git a/example/resources.conf.prod b/example/resources.conf.prod index 8a9a534e..64812547 100644 --- a/example/resources.conf.prod +++ b/example/resources.conf.prod @@ -114,3 +114,6 @@ modem: auth_algo: 'comp128v1' ciphers: [a5_0, a5_1] features: ['gprs', 'sim'] + +osmocon_phone: + - serial_device: '/dev/serial/by-id/usb-Silicon_Labs_CP2104_USB_to_UART_Bridge_Controller_00897B41-if00-port0' diff --git a/example/resources.conf.rnd b/example/resources.conf.rnd index 74832602..05fce872 100644 --- a/example/resources.conf.rnd +++ b/example/resources.conf.rnd @@ -87,3 +87,6 @@ modem: auth_algo: 'comp128v1' ciphers: [a5_0, a5_1] features: ['gprs', 'sim'] + +osmocon_phone: +- serial_device: '/dev/serial/by-id/usb-Silicon_Labs_CP2104_USB_to_UART_Bridge_Controller_0089279D-if00-port0' diff --git a/src/osmo_gsm_tester/osmocon.py b/src/osmo_gsm_tester/osmocon.py new file mode 100644 index 00000000..5b1e1458 --- /dev/null +++ b/src/osmo_gsm_tester/osmocon.py @@ -0,0 +1,103 @@ +# osmo_gsm_tester: specifics for running an osmocon +# +# Copyright (C) 2018 by sysmocom - s.f.m.c. GmbH +# +# Author: Pau Espin Pedrol +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import tempfile + +from . import log, util, process +from .event_loop import MainLoop + +class Osmocon(log.Origin): + suite_run = None + run_dir = None + process = None + sk_tmp_dir = None + + FIRMWARE_FILE="opt/osmocom-bb/target/firmware/board/compal_e88/layer1.compalram.bin" + + def __init__(self, suite_run, conf): + serial_device = conf.get('serial_device') + if serial_device is None: + raise log.Error('osmocon_phone contains no attr "serial_device"') + self.serial_device = os.path.realpath(serial_device) + super().__init__(log.C_RUN, 'osmocon_%s' % os.path.basename(self.serial_device)) + self.suite_run = suite_run + self.conf = conf + self.sk_tmp_dir = tempfile.mkdtemp('', 'ogtosmoconsk') + if len(self.l2_socket_path().encode()) > 107: + raise log.Error('Path for l2 socket is longer than max allowed len for unix socket path (107):', self.l2_socket_path()) + if len(self.loader_socket_path().encode()) > 107: + raise log.Error('Path for loader socket is longer than max allowed len for unix socket path (107):', self.loader_socket_path()) + + def l2_socket_path(self): + return os.path.join(self.sk_tmp_dir, 'osmocom_l2') + + def loader_socket_path(self): + return os.path.join(self.sk_tmp_dir, 'osmocom_loader') + + def start(self): + self.log('Resetting the phone') + # TODO: make sure the pone is powered off before starting osmocon + + self.log('Starting osmocon') + self.run_dir = util.Dir(self.suite_run.get_test_run_dir().new_dir(self.name())) + + inst = util.Dir(os.path.abspath(self.suite_run.trial.get_inst('osmocom-bb'))) + + binary = inst.child('sbin', 'osmocon') + if not os.path.isfile(binary): + raise RuntimeError('Binary missing: %r' % binary) + lib = inst.child('lib') + if not os.path.isdir(lib): + raise RuntimeError('No lib/ in %r' % inst) + + env = { 'LD_LIBRARY_PATH': util.prepend_library_path(lib) } + + firmware_path = os.path.join(str(inst), Osmocon.FIRMWARE_FILE) + if not os.path.isfile(firmware_path): + raise RuntimeError('Binary missing: %r' % firmware_path) + self.dbg(run_dir=self.run_dir, binary=binary, env=env) + self.process = process.Process(self.name(), self.run_dir, + (binary, '-p', self.serial_device, + '-m', 'c123xor', + '-s', self.l2_socket_path(), + '-l', self.loader_socket_path(), + firmware_path), + env=env) + self.suite_run.remember_to_stop(self.process) + self.process.launch() + self.log('Waiting for osmocon to be up and running') + MainLoop.wait(self, os.path.exists, self.l2_socket_path()) + + def running(self): + return not self.process.terminated() + + def cleanup(self): + if self.sk_tmp_dir: + try: + os.remove(self.l2_socket_path()) + except OSError: + pass + try: + os.remove(self.loader_socket_path()) + except OSError: + pass + os.rmdir(self.sk_tmp_dir) + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/src/osmo_gsm_tester/resource.py b/src/osmo_gsm_tester/resource.py index 70d6e8af..28c4117e 100644 --- a/src/osmo_gsm_tester/resource.py +++ b/src/osmo_gsm_tester/resource.py @@ -44,7 +44,8 @@ R_IP_ADDRESS = 'ip_address' R_BTS = 'bts' R_ARFCN = 'arfcn' R_MODEM = 'modem' -R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM) +R_OSMOCON = 'osmocon_phone' +R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON) RESOURCES_SCHEMA = { 'ip_address[].addr': schema.IPV4, @@ -76,6 +77,7 @@ RESOURCES_SCHEMA = { 'modem[].auth_algo': schema.AUTH_ALGO, 'modem[].ciphers[]': schema.CIPHER, 'modem[].features[]': schema.MODEM_FEATURE, + 'osmocon_phone[].serial_device': schema.STR, } WANT_SCHEMA = util.dict_add( diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py index 618a39ba..db4a8dc2 100644 --- a/src/osmo_gsm_tester/suite.py +++ b/src/osmo_gsm_tester/suite.py @@ -23,7 +23,7 @@ import time import pprint from . import config, log, template, util, resource, schema, test from .event_loop import MainLoop -from . import osmo_nitb, osmo_hlr, osmo_mgcpgw, osmo_mgw, osmo_msc, osmo_bsc, osmo_stp, osmo_ggsn, osmo_sgsn, modem, esme +from . import osmo_nitb, osmo_hlr, osmo_mgcpgw, osmo_mgw, osmo_msc, osmo_bsc, osmo_stp, osmo_ggsn, osmo_sgsn, modem, esme, osmocon class Timeout(Exception): pass @@ -332,6 +332,12 @@ class SuiteRun(log.Origin): self.register_for_cleanup(esme_obj) return esme_obj + def osmocon(self, specifics=None): + conf = self.reserved_resources.get(resource.R_OSMOCON, specifics=specifics) + osmocon_obj = osmocon.Osmocon(self, conf=conf) + self.register_for_cleanup(osmocon_obj) + return osmocon_obj + def msisdn(self): msisdn = self.resources_pool.next_msisdn(self) self.log('using MSISDN', msisdn) diff --git a/ttcn3/README.txt b/ttcn3/README.txt new file mode 100644 index 00000000..3886c801 --- /dev/null +++ b/ttcn3/README.txt @@ -0,0 +1,20 @@ +This directory contains a set of scripts and osmo-gsm-tester testsuites to run +osmo-ttcn3-hacks.git BTS_tests.ttcn (https://git.osmocom.org/osmo-ttcn3-hacks/tree/bts). + +The idea is to set up automatically the following components: +TTCN3 <-> osmocon (osmocom-bb) <-> motorola C123 <-> RF network <-> BTS_TO_TEST <-> TTCN3 + osmo-bsc + +* A jenkins job builds a docker image containing a built BTS_tests TTCN testsuite. +* Another jenkins job retrieves the artifacts from osmo-gsm-tester-build jobs + plus one for required osmocon binary. This job then calls osmo-gsm-tester/ttcn3/jenkins-run.sh, which will: +** Pull the above mentioned docker image containing BTS_Tests. +** Start osmo-gsm-tester with OSMO_GSM_TESTER_OPTS=osmo-gsm-tester/ttcn3/paths.conf, + that contains mostly same stuff as regular osmo-gsm-tester jobs, but with a + different testsuite containing 1 test "ttcn3_bts_tests.py". +** The test "ttcn3_bts_tests.py" does the following: +*** Start and manage all osmocom required components to run BTS_Tests: osmo-bts, osmo-bsc, osmocon, etc. +*** Generate the BTS_Tests.cfg required by BTS_Tests from a template to adapt to dynamic bits set by osmo-gsm-tester. +*** Launch script osmo-gsm-tester/ttcn3/suites/ttcn3_bts_tests/scripts/run_ttcn3_docker.sh with parameters and wait for it to finish. + This script will start and manage the lifecycle of the docker container running BTS_Tests + +See OS#3155 for more information regarding this topic. diff --git a/ttcn3/default-suites.conf b/ttcn3/default-suites.conf new file mode 100644 index 00000000..80f14e68 --- /dev/null +++ b/ttcn3/default-suites.conf @@ -0,0 +1 @@ +- ttcn3_bts_tests:trx diff --git a/ttcn3/defaults.conf b/ttcn3/defaults.conf new file mode 120000 index 00000000..e47699d2 --- /dev/null +++ b/ttcn3/defaults.conf @@ -0,0 +1 @@ +../example/defaults.conf \ No newline at end of file diff --git a/ttcn3/jenkins-run.sh b/ttcn3/jenkins-run.sh new file mode 100755 index 00000000..c7446065 --- /dev/null +++ b/ttcn3/jenkins-run.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e -x +base="$PWD" + +time_start="$(date '+%F %T')" + +prepare_docker() { + OLDPWD=$PWD + + # update docker-playground and update the BSC and bsc-test containers (if needed) + DIR=~/jenkins/docker-playground + if [ ! -d "$DIR" ]; then + mkdir -p ~/jenkins/ && cd ~/jenkins + git clone git://git.osmocom.org/docker-playground + fi + cd $DIR + git remote prune origin; git fetch; git checkout -f -B master origin/master + cd $DIR/debian-stretch-titan && make + docker pull laforge/debian-stretch-titan:latest # HACK + cd $DIR/ttcn3-bts-test && make + # execute the script to start containers, read results, ... + #cd $DIR/ttcn3-bts-test && sh -x ./jenkins.sh + PWD=$OLDPWD +} + +docker pull registry.sysmocom.de/ttcn3-bts-test + +# remove older trial dirs and *-run.tgz, if any +trial_dir_prefix="trial-" +rm -rf "$trial_dir_prefix"* || true + +# Expecting *.tgz artifacts to be copied to this workspace from the various +# jenkins-*.sh runs, via jenkins job configuration. Compose a trial dir: +trial_dir="${trial_dir_prefix}$BUILD_NUMBER" +mkdir -p "$trial_dir" + +mv *.tgz "$trial_dir" +cat *.md5 >> "$trial_dir/checksums.md5" +rm *.md5 + +# OSMO_GSM_TESTER_OPTS is a way to pass in e.g. logging preferences from the +# jenkins build job. +# On failure, first clean up below and then return the exit code. +exit_code="1" +if python3 -u "$(which osmo-gsm-tester.py)" "$trial_dir" $OSMO_GSM_TESTER_OPTS ; then + exit_code="0" +fi + +# no need to keep extracted binaries +rm -rf "$trial_dir/inst" || true + +# tar up all results for archiving (optional) +cd "$trial_dir" +journalctl -u ofono -o short-precise --since "${time_start}" > "$(readlink last_run)/ofono.log" +tar czf "$base/${trial_dir}-run.tgz" "$(readlink last_run)" +tar czf "$base/${trial_dir}-bin.tgz" *.md5 *.tgz + +exit $exit_code diff --git a/ttcn3/paths.conf b/ttcn3/paths.conf new file mode 100644 index 00000000..27c5818b --- /dev/null +++ b/ttcn3/paths.conf @@ -0,0 +1,3 @@ +state_dir: '/var/tmp/osmo-gsm-tester/state' +suites_dir: './suites' +scenarios_dir: './scenarios' diff --git a/ttcn3/resources.conf.prod b/ttcn3/resources.conf.prod new file mode 120000 index 00000000..3e40e89a --- /dev/null +++ b/ttcn3/resources.conf.prod @@ -0,0 +1 @@ +../example/resources.conf.prod \ No newline at end of file diff --git a/ttcn3/resources.conf.rnd b/ttcn3/resources.conf.rnd new file mode 120000 index 00000000..6f984741 --- /dev/null +++ b/ttcn3/resources.conf.rnd @@ -0,0 +1 @@ +../example/resources.conf.rnd \ No newline at end of file diff --git a/ttcn3/scenarios/trx.conf b/ttcn3/scenarios/trx.conf new file mode 120000 index 00000000..d72ddb24 --- /dev/null +++ b/ttcn3/scenarios/trx.conf @@ -0,0 +1 @@ +../../example/scenarios/trx.conf \ No newline at end of file diff --git a/ttcn3/suites/ttcn3_bts_tests/scripts/BTS_Tests.cfg.tmpl b/ttcn3/suites/ttcn3_bts_tests/scripts/BTS_Tests.cfg.tmpl new file mode 100644 index 00000000..73795289 --- /dev/null +++ b/ttcn3/suites/ttcn3_bts_tests/scripts/BTS_Tests.cfg.tmpl @@ -0,0 +1,25 @@ +[ORDERED_INCLUDE] +"/osmo-ttcn3-hacks/Common.cfg" +"/osmo-ttcn3-hacks/bts/BTS_Tests.default" + +[LOGGING] + +[TESTPORT_PARAMETERS] +*.BTSVTY.CTRL_HOSTNAME := "${btsvty_ctrl_hostname}" + +[MODULE_PARAMETERS] +BTS_Tests.mp_rsl_ip := "172.18.9.10" +BTS_Tests.mp_bb_trxc_ip := "127.0.0.1" +BTS_Tests.mp_pcu_socket := "/data/unix_pcu/pcu_bts" +BTS_Tests.mp_bb_trxc_port := -1 +L1CTL_PortType.m_l1ctl_sock_path := "/data/unix_l2/osmocom_l2" +BTS_Tests.mp_ctrl_ip := "${btsvty_ctrl_hostname}" +BTS_Tests.mp_rxlev_exp := 1 +BTS_Tests.mp_tolerance_rxlev := 10; +BTS_Tests.mp_tolerance_rxqual := 1; +BTS_Tests.mp_trx0_arfcn := 868 + +[MAIN_CONTROLLER] + +[EXECUTE] +BTS_Tests.control diff --git a/ttcn3/suites/ttcn3_bts_tests/scripts/run_ttcn3_docker.sh b/ttcn3/suites/ttcn3_bts_tests/scripts/run_ttcn3_docker.sh new file mode 100755 index 00000000..64987b35 --- /dev/null +++ b/ttcn3/suites/ttcn3_bts_tests/scripts/run_ttcn3_docker.sh @@ -0,0 +1,86 @@ +#!/bin/sh +set -x + +RUNDIR="$1" +JUNIT_TTCN3_DST_FILE="$2" +L2_SOCKET_PATH="$3" +PCU_SOCKET_PATH="$4" + +# Absolute path to this script +SCRIPT=$(readlink -f "$0") +# Absolute path this script is in +SCRIPTPATH=$(dirname "$SCRIPT") + +VOL_BASE_DIR="$RUNDIR/logs" +rm -rf "$VOL_BASE_DIR" +mkdir -p "$VOL_BASE_DIR" + +if [ "x$BUILD_TAG" = "x" ]; then + BUILD_TAG=nonjenkins +fi + +REPO_USER="registry.sysmocom.de" +SUITE_NAME="ttcn3-bts-test" +NET_NAME=$SUITE_NAME +DOCKER_NAME="$BUILD_TAG-$SUITE_NAME" + +network_create() { + NET=$1 + echo Creating network $NET_NAME + docker network create --internal --subnet $NET $NET_NAME +} + +network_remove() { + echo Removing network $NET_NAME + docker network remove $NET_NAME +} + +child_ps=0 +forward_kill() { + sig="$1" + echo "Caught signal SIG$sig!" + if [ "$child_ps" != "0" ]; then + echo "Killing $child_ps with SIG$sig!" + docker kill ${DOCKER_NAME} + fi + exit 130 +} +forward_kill_int() { + forward_kill "INT" +} +forward_kill_term() { + forward_kill "TERM" +} +# Don't use 'set -e', otherwise traps are not triggered! +trap forward_kill_int INT +trap forward_kill_term TERM + +network_create 172.18.9.0/24 + +mkdir $VOL_BASE_DIR/bts-tester +echo "SCRIPTPATH=$SCRIPTPATH PWD=$PWD" +cp $RUNDIR/BTS_Tests.cfg $VOL_BASE_DIR/bts-tester/ + +echo Starting container with BTS testsuite +docker kill ${DOCKER_NAME} +docker run --rm \ + --network $NET_NAME --ip 172.18.9.10 \ + -e "TTCN3_PCAP_PATH=/data" \ + --mount type=bind,source=$VOL_BASE_DIR/bts-tester,destination=/data \ + --mount type=bind,source="$(dirname "$L2_SOCKET_PATH")",destination=/data/unix_l2 \ + --mount type=bind,source="$(dirname "$PCU_SOCKET_PATH")",destination=/data/unix_pcu \ + --name ${DOCKER_NAME} \ + $REPO_USER/${SUITE_NAME} & +child_ps=$! +echo "$$: waiting for $child_ps" +wait "$child_ps" +child_exit_code="$?" +echo "ttcn3 docker exited with code $child_exit_code" + +network_remove + +echo "Copying TTCN3 junit file to $JUNIT_TTCN3_DST_FILE" +cp $VOL_BASE_DIR/bts-tester/junit-xml-*.log $JUNIT_TTCN3_DST_FILE +sed -i "s#classname='BTS_Tests'#classname='$(basename $JUNIT_TTCN3_DST_FILE)'#g" $JUNIT_TTCN3_DST_FILE + +exit $child_exit_code diff --git a/ttcn3/suites/ttcn3_bts_tests/suite.conf b/ttcn3/suites/ttcn3_bts_tests/suite.conf new file mode 100644 index 00000000..1eb0a020 --- /dev/null +++ b/ttcn3/suites/ttcn3_bts_tests/suite.conf @@ -0,0 +1,7 @@ +resources: + ip_address: + - times: 7 # msc, bsc, hlr, stp, mgw, sgsn, ggsn + bts: + - times: 1 + osmocon_phone: + - times: 1 diff --git a/ttcn3/suites/ttcn3_bts_tests/ttcn3_bts_tests.py b/ttcn3/suites/ttcn3_bts_tests/ttcn3_bts_tests.py new file mode 100755 index 00000000..b3ca9462 --- /dev/null +++ b/ttcn3/suites/ttcn3_bts_tests/ttcn3_bts_tests.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import os +from mako.template import Template + +from osmo_gsm_tester.testenv import * + +hlr_dummy = suite.hlr() +mgw_dummy = suite.mgw() +stp_dummy = suite.stp() +msc_dummy = suite.msc(hlr_dummy, mgw_dummy, stp_dummy) +ggsn_dummy = suite.ggsn() +sgsn_dummy = suite.sgsn(hlr_dummy, ggsn_dummy) +bsc = suite.bsc(msc_dummy, mgw_dummy, stp_dummy) +bts = suite.bts() +osmocon = suite.osmocon() + +bts.set_num_trx(1) +bts.set_trx_phy_channel(0, 0, 'CCCH+SDCCH4') +bts.set_trx_phy_channel(0, 1, 'TCH/F') +bts.set_trx_phy_channel(0, 2, 'TCH/F') +bts.set_trx_phy_channel(0, 3, 'TCH/F_PDCH') +bts.set_trx_phy_channel(0, 4, 'TCH/F_TCH/H_PDCH') +bts.set_trx_phy_channel(0, 5, 'TCH/H') +bts.set_trx_phy_channel(0, 6, 'SDCCH8') +bts.set_trx_phy_channel(0, 7, 'PDCH') + +print('Starting CNI') +hlr_dummy.start() +stp_dummy.start() +msc_dummy.start() +mgw_dummy.start() + +bsc.set_rsl_ip('172.18.9.10') +bsc.bts_add(bts) +sgsn_dummy.bts_add(bts) + +bsc.start() +bts.start(keepalive=True) + +print('Starting osmocon') +osmocon.start() + +own_dir = os.path.dirname(os.path.realpath(__file__)) +script_file = os.path.join(own_dir, 'scripts', 'run_ttcn3_docker.sh') +bts_tmpl_file = os.path.join(own_dir, 'scripts', 'BTS_Tests.cfg.tmpl') +script_run_dir = test.get_run_dir().new_dir('ttcn3') +bts_cfg_file = os.path.join(str(script_run_dir), 'BTS_Tests.cfg') +junit_ttcn3_dst_file = os.path.join(str(suite.trial.get_run_dir()), 'trial-') + suite.name() + '.xml' +docker_cmd = (script_file, str(script_run_dir), junit_ttcn3_dst_file, osmocon.l2_socket_path(), bts.pcu_socket_path()) + +print('Creating template') +mytemplate = Template(filename=bts_tmpl_file) +r = mytemplate.render(btsvty_ctrl_hostname=bts.remote_addr()) +with open(bts_cfg_file, 'w') as f: + f.write(r) + + +print('Starting TTCN3 tests') +proc = process.Process('ttcn3', script_run_dir, docker_cmd) +try: + proc.launch() + print('Starting TTCN3 launched, waiting until it finishes') + proc.wait(timeout=3600) +except Exception as e: + proc.terminate() + raise e + +if proc.result != 0: + raise RuntimeError("run_ttcn3_docker.sh exited with error code %d" % proc.result) + +print('Done')