Introduce support for AmarisoftEPC

* A new abstract generic base class EPC is created
* srsEPC and AmarisoftEPC inherit from that class
* options are loaded from defaults.conf in cascade. First generic "epc",
  afterwards the specific enb type.
* A new scenario is added to select the EPC type to use. srsEPC is the
  default unless stated by an scenario.
* AmarisoftEPC delegates setup of the tun IP address to an "ifup"
  script. As a result, since we run without root permissions (ony with
  CAP_NET_ADMIN), the ifup script itself is unablet o set the IP
  address. To solve this, we introduce a new osmo-gsm-tester helper
  script which must be installed in the slave node which can be called
  through sudo to increase privileges to do so.

With this commit, I can already get srsUE<->amarisoftENB<->amarisoftEPC
to pass ping and iperf3 4g tests.

Change-Id: Ia50ea6a74b63b2d688c8d683aea11416ad40a6d3
This commit is contained in:
Pau Espin 2020-03-31 13:45:01 +02:00
parent 55e278c758
commit da2e31f929
10 changed files with 451 additions and 21 deletions

View File

@ -93,12 +93,17 @@ osmo_bts_oc2g:
trx_list:
- nominal_power: 25
srsepc:
epc:
type: srsepc
mcc: 901
mnc: 70
srsepc:
rlc_drb_mode: UM
enable_pcap: false
amarisoftepc:
license_server_addr: 10.12.1.139
enb:
mcc: 901
mnc: 70

View File

@ -0,0 +1,3 @@
config:
epc:
type: ${param1}

View File

@ -0,0 +1,203 @@
# osmo_gsm_tester: specifics for running an SRS EPC process
#
# Copyright (C) 2020 by sysmocom - s.f.m.c. GmbH
#
# Author: Pau Espin Pedrol <pespin@sysmocom.de>
#
# 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 <http://www.gnu.org/licenses/>.
import os
import pprint
from . import log, util, config, template, process, remote
from . import epc
#def rlc_drb_mode2qci(rlc_drb_mode):
# if rlc_drb_mode.upper() == "UM":
# return 7;
# elif rlc_drb_mode.upper() == "AM":
# return 9;
# raise log.Error('Unexpected rlc_drb_mode', rlc_drb_mode=rlc_drb_mode)
class AmarisoftEPC(epc.EPC):
REMOTE_DIR = '/osmo-gsm-tester-amarisoftepc'
BINFILE = 'ltemme'
CFGFILE = 'amarisoft_ltemme.cfg'
LOGFILE = 'ltemme.log'
IFUPFILE = 'mme-ifup'
def __init__(self, suite_run, run_node):
super().__init__(suite_run, run_node, 'amarisoftepc')
self.run_dir = None
self.config_file = None
self.log_file = None
self.ifup_file = None
self.process = None
self.rem_host = None
self.remote_inst = None
self.remote_config_file = None
self.remote_log_file = None
self.remote_ifup_file =None
self._bin_prefix = None
self.inst = None
self.subscriber_list = []
def bin_prefix(self):
if self._bin_prefix is None:
self._bin_prefix = os.getenv('AMARISOFT_PATH_EPC', AmarisoftEPC.REMOTE_DIR)
return self._bin_prefix
def cleanup(self):
if self.process is None:
return
if self._run_node.is_local():
return
# copy back files (may not exist, for instance if there was an early error of process):
try:
self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
except Exception as e:
self.log(repr(e))
def start(self):
self.log('Starting amarisoftepc')
self.run_dir = util.Dir(self.suite_run.get_test_run_dir().new_dir(self.name()))
self.configure()
if self._run_node.is_local():
self.start_locally()
else:
self.start_remotely()
def start_remotely(self):
remote_binary = self.remote_inst.child('', AmarisoftEPC.BINFILE)
# setting capabilities will later disable use of LD_LIBRARY_PATH from ELF loader -> modify RPATH instead.
self.log('Setting RPATH for amarisoftepc')
self.rem_host.change_elf_rpath(remote_binary, str(self.remote_inst))
# amarisoftepc requires CAP_NET_ADMIN to create tunnel devices: ioctl(TUNSETIFF):
self.log('Applying CAP_NET_ADMIN capability to amarisoftepc')
self.rem_host.setcap_net_admin(remote_binary)
args = (remote_binary, self.remote_config_file)
self.process = self.rem_host.RemoteProcess(AmarisoftEPC.BINFILE, args)
#self.process = self.rem_host.RemoteProcessFixIgnoreSIGHUP(AmarisoftEPC.BINFILE, remote_run_dir, args)
self.suite_run.remember_to_stop(self.process)
self.process.launch()
def start_locally(self):
binary = self.inst.child('', BINFILE)
env = {}
# setting capabilities will later disable use of LD_LIBRARY_PATH from ELF loader -> modify RPATH instead.
self.log('Setting RPATH for amarisoftepc')
util.change_elf_rpath(binary, util.prepend_library_path(str(self.inst)), self.run_dir.new_dir('patchelf'))
# amarisoftepc requires CAP_NET_ADMIN to create tunnel devices: ioctl(TUNSETIFF):
self.log('Applying CAP_NET_ADMIN capability to amarisoftepc')
util.setcap_net_admin(binary, self.run_dir.new_dir('setcap_net_admin'))
self.dbg(run_dir=self.run_dir, binary=binary, env=env)
args = (binary, os.path.abspath(self.config_file))
self.process = process.Process(self.name(), self.run_dir, args, env=env)
self.suite_run.remember_to_stop(self.process)
self.process.launch()
def configure(self):
self.inst = util.Dir(os.path.abspath(self.bin_prefix()))
if not self.inst.isfile('', AmarisoftEPC.BINFILE):
raise log.Error('No %s binary in' % AmarisoftEPC.BINFILE, self.inst)
self.config_file = self.run_dir.child(AmarisoftEPC.CFGFILE)
self.log_file = self.run_dir.child(AmarisoftEPC.LOGFILE)
self.ifup_file = self.run_dir.new_file(AmarisoftEPC.IFUPFILE)
os.chmod(self.ifup_file, 0o744) # add execution permission
self.dbg(config_file=self.config_file)
with open(self.ifup_file, 'w') as f:
r = '''#!/bin/sh
set -x -e
# script + sudoers file available in osmo-gsm-tester.git/utils/{bin,sudoers.d}
sudo /usr/local/bin/osmo-gsm-tester_amarisoft_ltemme_ifup.sh "$@"
'''
f.write(r)
if not self._run_node.is_local():
self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
remote_prefix_dir = util.Dir(AmarisoftEPC.REMOTE_DIR)
self.remote_inst = util.Dir(remote_prefix_dir.child(os.path.basename(str(self.inst))))
remote_run_dir = util.Dir(remote_prefix_dir.child(AmarisoftEPC.BINFILE))
self.remote_config_file = remote_run_dir.child(AmarisoftEPC.CFGFILE)
self.remote_log_file = remote_run_dir.child(AmarisoftEPC.LOGFILE)
self.remote_ifup_file = remote_run_dir.child(AmarisoftEPC.IFUPFILE)
values = super().configure('amarisoftepc')
logfile = self.log_file if self._run_node.is_local() else self.remote_log_file
ifupfile = self.ifup_file if self._run_node.is_local() else self.remote_ifup_file
config.overlay(values, dict(epc=dict(log_filename=logfile,
ifup_filename=ifupfile)))
# Set qci for each subscriber:
#rlc_drb_mode = values['epc'].get('rlc_drb_mode', None)
#assert rlc_drb_mode is not None
#for i in range(len(self.subscriber_list)):
# self.subscriber_list[i]['qci'] = rlc_drb_mode2qci(rlc_drb_mode)
config.overlay(values, dict(epc=dict(hss=dict(subscribers=self.subscriber_list))))
self.dbg('SRSEPC CONFIG:\n' + pprint.pformat(values))
with open(self.config_file, 'w') as f:
r = template.render(AmarisoftEPC.CFGFILE, values)
self.dbg(r)
f.write(r)
if not self._run_node.is_local():
self.rem_host.recreate_remote_dir(self.remote_inst)
self.rem_host.scp('scp-inst-to-remote', str(self.inst), remote_prefix_dir)
self.rem_host.recreate_remote_dir(remote_run_dir)
self.rem_host.scp('scp-cfg-to-remote', self.config_file, self.remote_config_file)
self.rem_host.scp('scp-ifup-to-remote', self.ifup_file, self.remote_ifup_file)
def subscriber_add(self, modem, msisdn=None, algo_str=None):
if msisdn is None:
msisdn = self.suite_run.resources_pool.next_msisdn(modem)
modem.set_msisdn(msisdn)
if algo_str is None:
algo_str = modem.auth_algo() or util.OSMO_AUTH_ALGO_NONE
if algo_str != util.OSMO_AUTH_ALGO_NONE and not modem.ki():
raise log.Error("Auth algo %r selected but no KI specified" % algo_str)
subscriber_id = len(self.subscriber_list) # list index
self.subscriber_list.append({'id': subscriber_id, 'imsi': modem.imsi(), 'msisdn': msisdn, 'auth_algo': algo_str, 'ki': modem.ki(), 'opc': None, 'apn_ipaddr': modem.apn_ipaddr()})
self.log('Add subscriber', msisdn=msisdn, imsi=modem.imsi(), subscriber_id=subscriber_id,
algo_str=algo_str)
return subscriber_id
def enb_is_connected(self, enb):
# TODO: improve this a bit, like matching IP addr of enb. CTRL iface?
# The string is only available in log file, not in stdout:
#return 'S1 setup response' in (self.process.get_stdout() or '')
return True
def running(self):
return not self.process.terminated()
def tun_addr(self):
# TODO: set proper addr
return '192.168.4.1'
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,80 @@
# osmo_gsm_tester: base classes to share code among EPC subclasses.
#
# Copyright (C) 2020 by sysmocom - s.f.m.c. GmbH
#
# Author: Pau Espin Pedrol <pespin@sysmocom.de>
#
# 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 <http://www.gnu.org/licenses/>.
from abc import ABCMeta, abstractmethod
from . import log, config
class EPC(log.Origin, metaclass=ABCMeta):
##############
# PROTECTED
##############
def __init__(self, suite_run, run_node, name):
super().__init__(log.C_RUN, '%s' % name)
self._addr = run_node.run_addr()
self.set_name('%s_%s' % (name, self._addr))
self.suite_run = suite_run
self._run_node = run_node
def configure(self, default_specifics):
values = dict(epc=config.get_defaults('epc'))
config.overlay(values, dict(epc=config.get_defaults(default_specifics)))
config.overlay(values, dict(epc=self.suite_run.config().get('epc', {})))
config.overlay(values, dict(epc={'run_addr': self.addr()}))
return values
########################
# PUBLIC - INTERNAL API
########################
def cleanup(self):
'Nothing to do by default. Subclass can override if required.'
pass
###################
# PUBLIC (test API included)
###################
@abstractmethod
def start(self, epc):
'Starts ENB, it will connect to "epc"'
pass
@abstractmethod
def subscriber_add(self, modem, msisdn=None, algo_str=None):
pass
@abstractmethod
def enb_is_connected(self, enb):
pass
@abstractmethod
def running(self):
pass
@abstractmethod
def tun_addr(self):
pass
def addr(self):
return self._addr
def run_node(self):
return self._run_node
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -29,7 +29,7 @@ from . import schema
from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts, bts_oc2g
from . import modem
from . import ms_osmo_mobile
from . import srs_ue, srs_enb, amarisoft_enb
from . import srs_ue, srs_enb, amarisoft_enb, srs_epc, amarisoft_epc
from .util import is_dict, is_list
@ -122,6 +122,7 @@ CONF_SCHEMA = util.dict_add(
{ 'defaults.timeout': schema.STR,
'config.bsc.net.codec_list[]': schema.CODEC,
'config.enb.enable_pcap': schema.BOOL_STR,
'config.epc.type': schema.STR,
'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE,
'config.epc.enable_pcap': schema.BOOL_STR,
'config.modem.enable_pcap': schema.BOOL_STR,
@ -143,6 +144,11 @@ KNOWN_ENB_TYPES = {
'amarisoftenb': amarisoft_enb.AmarisoftENB,
}
KNOWN_EPC_TYPES = {
'srsepc': srs_epc.srsEPC,
'amarisoftepc': amarisoft_epc.AmarisoftEPC,
}
KNOWN_MS_TYPES = {
# Map None to ofono for forward compability
None: modem.Modem,

View File

@ -21,6 +21,7 @@ import os
import pprint
from . import log, util, config, template, process, remote
from . import epc
def rlc_drb_mode2qci(rlc_drb_mode):
if rlc_drb_mode.upper() == "UM":
@ -29,7 +30,7 @@ def rlc_drb_mode2qci(rlc_drb_mode):
return 9;
raise log.Error('Unexpected rlc_drb_mode', rlc_drb_mode=rlc_drb_mode)
class srsEPC(log.Origin):
class srsEPC(epc.EPC):
REMOTE_DIR = '/osmo-gsm-tester-srsepc'
BINFILE = 'srsepc'
@ -39,9 +40,7 @@ class srsEPC(log.Origin):
LOGFILE = 'srsepc.log'
def __init__(self, suite_run, run_node):
super().__init__(log.C_RUN, 'srsepc')
self._addr = run_node.run_addr()
self.set_name('srsepc_%s' % self._addr)
super().__init__(suite_run, run_node, 'srsepc')
self.run_dir = None
self.config_file = None
self.db_file = None
@ -55,8 +54,6 @@ class srsEPC(log.Origin):
self.remote_pcap_file = None
self.enable_pcap = False
self.subscriber_list = []
self.suite_run = suite_run
self._run_node = run_node
def cleanup(self):
if self.process is None:
@ -161,9 +158,7 @@ class srsEPC(log.Origin):
self.pcap_file = self.run_dir.child(srsEPC.PCAPFILE)
self.dbg(config_file=self.config_file, db_file=self.db_file)
values = dict(epc=config.get_defaults('srsepc'))
config.overlay(values, dict(epc=self.suite_run.config().get('epc', {})))
config.overlay(values, dict(epc={'run_addr': self.addr()}))
values = super().configure('srsepc')
# Convert parsed boolean string to Python boolean:
self.enable_pcap = util.str2bool(values['epc'].get('enable_pcap', 'false'))
@ -212,13 +207,7 @@ class srsEPC(log.Origin):
def running(self):
return not self.process.terminated()
def addr(self):
return self._addr
def tun_addr(self):
return '172.16.0.1'
def run_node(self):
return self._run_node
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -25,7 +25,6 @@ from . import config, log, util, resource, 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, esme, osmocon, ms_driver, iperf3, process
from . import run_node
from . import srs_epc
class Timeout(Exception):
pass
@ -374,9 +373,9 @@ class SuiteRun(log.Origin):
def epc(self, run_node=None):
if run_node is None:
run_node = self.run_node()
epc_obj = srs_epc.srsEPC(self, run_node)
self.register_for_cleanup(epc_obj)
return epc_obj
epc = epc_obj(self, run_node)
self.register_for_cleanup(epc)
return epc
def osmocon(self, specifics=None):
conf = self.reserved_resources.get(resource.R_OSMOCON, specifics=specifics)
@ -505,4 +504,16 @@ def enb_obj(suite_run, conf):
raise RuntimeError('No such ENB type is defined: %r' % enb_type)
return enb_class(suite_run, conf)
def epc_obj(suite_run, run_node):
values = dict(epc=config.get_defaults('epc'))
config.overlay(values, dict(epc=suite_run.config().get('epc', {})))
epc_type = values['epc'].get('type', None)
if epc_type is None:
raise RuntimeError('EPC type is not defined!')
log.dbg('create EPC object', type=epc_type)
epc_class = resource.KNOWN_EPC_TYPES.get(epc_type)
if epc_class is None:
raise RuntimeError('No such EPC type is defined: %r' % epc_type)
return epc_class(suite_run, run_node)
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,116 @@
/* ltemme configuration file
* version 2018-10-18
* Copyright (C) 2015-2018 Amarisoft
*/
{
license_server: {
server_addr: "${epc.license_server_addr}",
name: "amarisoft",
},
/* Log filter: syntax: layer.field=value[,...]
Possible layers are nas, ip, s1ap, gtpu and all. The 'all' layer
is used to address all the layers at the same time.
field values:
- 'level': the log level of each layer can be set to 'none',
'error', 'info' or 'debug'. Use 'debug' to log all the messages.
- 'max_size': set the maximum size of the hex dump. 0 means no
hex dump. -1 means no limit.
*/
//log_options: "all.level=debug,all.max_size=32",
log_options: "all.level=error,all.max_size=0,nas.level=debug,nas.max_size=1,s1ap.level=debug,s1ap.max_size=1",
log_filename: "${epc.log_filename}",
/* Enable remote API and Web interface */
com_addr: "${epc.run_addr}:9000",
/* bind address for GTP-U. Normally = address of the PC, here bound
on local interface to be able to run ltemme on the same PC as
lteenb. By default, the S1AP SCTP connection is bound on the same
address. */
gtp_addr: "${epc.run_addr}",
s1ap_bind_addr: "${epc.run_addr}",
plmn: "${epc.mcc}${epc.mnc}",
mme_group_id: 32769,
mme_code: 1,
/* network name and network short name sent in the EMM information
message to the UE */
network_name: "Amarisoft Network",
network_short_name: "Amarisoft",
/* Control Plane Cellular IoT EPS optimization support */
cp_ciot_opt: true,
/* Public Data Networks. The first one is the default. */
pdn_list: [
{
/* Some UE requires a specific PDN for data access */
pdn_type: "ipv4",
access_point_name: "internet",
first_ip_addr: "192.168.4.2",
last_ip_addr: "192.168.4.254",
ip_addr_shift: 2,
dns_addr: "8.8.8.8", /* Google DNS address */
erabs: [
{
qci: 9,
priority_level: 15,
pre_emption_capability: "shall_not_trigger_pre_emption",
pre_emption_vulnerability: "not_pre_emptable",
},
],
},
],
/* Setup script for the network interface.
If no script is given, no network interface is created.
Script is called for each PDN with following parameters:
1) Interface name
2) PDN index
3) Access Point Name
4) IP version: 'ipv4' or 'ipv6'
5) IP address: first IP address for ipv4 and link local address for IPv6
6) First IP address
7) Last IP address
*/
tun_setup_script: "${epc.ifup_filename}",
/* If true, inter-UE routing is done inside the MME (hence no IP
packet is output to the virtual network interface in case of
inter-UE communication). Otherwise, it is done by the Linux IP
layer. */
ue_to_ue_forwarding: false,
/* NAS ciphering algorithm preference. EEA0 is always the last. */
nas_cipher_algo_pref: [ ],
/* NAS integrity algorithm preference. EIA0 is always the last. */
nas_integ_algo_pref: [ 2, 1 ],
/* user data base */
ue_db: [
%for sub in epc.hss.subscribers:
{
sim_algo: "${sub.auth_algo}", /* USIM authentication algorithm: xor, milenage or tuak */
imsi: "${sub.imsi}", /* Anritsu Test USIM */
amf: 0x9001, /* Authentication Management Field */
sqn: "000000000000", /* Sequence Number */
K: "${sub.ki}", /* Anritsu Test USIM */
/* if true, allow several UEs to have the same IMSI (useful
with test SIM cards). They are distinguished with their
IMEI. default = false. */
multi_sim: true,
},
%endfor
/* Add new entries for each IMSI/K */
],
/* persistent user database */
//ue_db_filename: "lte_ue.db",
}

View File

@ -0,0 +1,16 @@
#!/bin/bash -e
ifname="$1" # Interface name
index="$2" # Network index (PDN index)
apn="$3" # Access point name
type="$4" # ipv4 or ipv6
ifaddr="$5" # Interface address
addr1="$6" # First IP address
addr2="$7" # Last IP address
mask="$8" # Mask
echo "*** Configuring $type APN[$index] '$apn' on ${ifname}, $ifaddr/$mask, ${addr1}..${addr2}"
if [ "$type" = "ipv4" ] ; then
ifconfig ${ifname} ${ifaddr}/${mask} up
else
ifconfig ${ifname} inet6 add ${addr1}/${mask} up
fi
echo "*** done configuring interface ${ifname}"

View File

@ -0,0 +1 @@
%osmo-gsm-tester ALL=(root) NOPASSWD: /usr/local/bin/osmo-gsm-tester_amarisoft_ltemme_ifup.sh