OsmoCtrl cleanup: get_var(), set_var(), get_trap()

CTRL interface interaction was mostly inherited from the first legacy
implementation of osmo-gsm-tester, and it was a pain to look at from the
start. Now, while I'm close to the topic, I want this to improve:

Properly match a GET_REPLY/SET_REPLY to a sent GET/SET by the message
ID.

Completely drop the do_get() and do_set(), which were not useful for
correct handling of the CTRL request and response messaging. The API to
use by callers is set_var(), get_var()/get_int_var() and get_trap().
These call the internal _sendrecv() (or for TRAP only _recv())
functions. Make it so that tese work both on an already connected
OsmoCtrl, as well as one that needs to establish a (short) connection,
so that both are trivially possible:

    # one CTRL connection stays open
    with OsmoCtrl(...) as ctrl:
  	ctrl.get_var('var1')
  	ctrl.get_var('var2')
  	ctrl.get_var('var3')

and

  # get_var() opens a connection, does the GET and closes again
  OsmoCtrl(...).get_var('var1')

Do away with doubling the instances OsmoCtrl and e.g. OsmoBscCtrl.
Rather make OsmoBscCtrl a child class of OsmoCtrl, which means that we
no longer have bsc.ctrl().ctrl(), just bsc.ctrl().

Have VERB_* constants instead of dup'd strings.

Apply to / simplify all callers of OsmoCtrl.

Some of these changes are similar to recently added OsmoVty.

Change-Id: Id561e5a55d8057a997a8ec9e7fa6f94840194df1
This commit is contained in:
Neels Hofmeyr 2020-12-01 04:48:38 +01:00
parent f79a86fc25
commit 5b04ef213e
4 changed files with 186 additions and 127 deletions

View File

@ -149,8 +149,10 @@ class OsmoBsc(log.Origin):
# over this list, we have a 1:1 match in indexes.
return self.bts.index(bts)
def bts_is_connected(self, bts):
return OsmoBscCtrl(self).bts_is_connected(self.bts_num(bts))
def bts_is_connected(self, bts, use_ctrl=None):
if use_ctrl is None:
use_ctrl = self.ctrl()
return use_ctrl.bts_is_connected(self.bts_num(bts))
def running(self):
return not self.process.terminated()
@ -160,32 +162,16 @@ class OsmoBsc(log.Origin):
self.vty.disconnect()
self.vty = None
class OsmoBscCtrl(log.Origin):
PORT = 4249
BTS_OML_STATE_VAR = "bts.%d.oml-connection-state"
BTS_OML_STATE_RE = re.compile("GET_REPLY (\d+) bts.\d+.oml-connection-state (?P<oml_state>\w+)")
def __init__(self, bsc):
self.bsc = bsc
super().__init__(log.C_BUS, 'CTRL(%s:%d)' % (self.bsc.addr(), OsmoBscCtrl.PORT))
def ctrl(self):
return osmo_ctrl.OsmoCtrl(self.bsc.addr(), OsmoBscCtrl.PORT)
return OsmoBscCtrl(self)
class OsmoBscCtrl(osmo_ctrl.OsmoCtrl):
def __init__(self, bsc, port=4249):
self.bsc = bsc
super().__init__(bsc.addr(), port)
def bts_is_connected(self, bts_num):
with self.ctrl() as ctrl:
ctrl.do_get(OsmoBscCtrl.BTS_OML_STATE_VAR % bts_num)
data = ctrl.receive()
while (len(data) > 0):
(answer, data) = ctrl.remove_ipa_ctrl_header(data)
answer_str = answer.decode('utf-8')
answer_str = answer_str.replace('\n', ' ')
res = OsmoBscCtrl.BTS_OML_STATE_RE.match(answer_str)
if res:
oml_state = str(res.group('oml_state'))
if oml_state == 'connected':
return True
return False
return self.get_var('bts.%d.oml-connection-state' % bts_num) == 'connected'
class OsmoBscVty(osmo_vty.OsmoVty):
def __init__(self, bsc, port=4242):

View File

@ -157,29 +157,12 @@ class OsmoMsc(log.Origin):
return not self.process.terminated()
class OsmoMscCtrl(log.Origin):
PORT = 4255
SUBSCR_LIST_ACTIVE_VAR = 'subscriber-list-active-v1'
def __init__(self, msc):
class OsmoMscCtrl(osmo_ctrl.OsmoCtrl):
def __init__(self, msc, port=4255):
self.msc = msc
super().__init__(log.C_BUS, 'CTRL(%s:%d)' % (self.msc.addr(), self.PORT))
def ctrl(self):
return osmo_ctrl.OsmoCtrl(self.msc.addr(), self.PORT)
super().__init__(self.msc.addr(), port)
def subscriber_list_active(self):
aslist_str = ""
with self.ctrl() as ctrl:
ctrl.do_get(self.SUBSCR_LIST_ACTIVE_VAR)
# This is legacy code from the old osmo-gsm-tester.
# looks like this doesn't work for long data.
data = ctrl.receive()
while (len(data) > 0):
(answer, data) = ctrl.remove_ipa_ctrl_header(data)
answer_str = answer.decode('utf-8')
answer_str = answer_str.replace('\n', ' ')
aslist_str = answer_str
return aslist_str
return self.get_var('subscriber-list-active-v1').replace('\n', ' ')
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -158,22 +158,10 @@ class OsmoNitb(log.Origin):
return not self.process.terminated()
class OsmoNitbCtrl(log.Origin):
PORT = 4249
SUBSCR_MODIFY_VAR = 'subscriber-modify-v1'
SUBSCR_MODIFY_REPLY_RE = re.compile("SET_REPLY (\d+) %s OK" % SUBSCR_MODIFY_VAR)
SUBSCR_DELETE_VAR = 'subscriber-delete-v1'
SUBSCR_DELETE_REPLY_RE = re.compile("SET_REPLY (\d+) %s Removed" % SUBSCR_DELETE_VAR)
SUBSCR_LIST_ACTIVE_VAR = 'subscriber-list-active-v1'
BTS_OML_STATE_VAR = "bts.%d.oml-connection-state"
BTS_OML_STATE_RE = re.compile("GET_REPLY (\d+) bts.\d+.oml-connection-state (?P<oml_state>\w+)")
def __init__(self, nitb):
class OsmoNitbCtrl(osmo_ctrl.OsmoCtrl):
def __init__(self, nitb, port=4249):
self.nitb = nitb
super().__init__(log.C_BUS, 'CTRL(%s:%d)' % (self.nitb.addr(), OsmoNitbCtrl.PORT))
def ctrl(self):
return osmo_ctrl.OsmoCtrl(self.nitb.addr(), OsmoNitbCtrl.PORT)
super().__init__(nitb.addr(), port)
def subscriber_add(self, imsi, msisdn, ki=None, algo=None):
if algo:
@ -181,54 +169,17 @@ class OsmoNitbCtrl(log.Origin):
else:
value = '%s,%s' % (imsi, msisdn)
with self.ctrl() as ctrl:
ctrl.do_set(OsmoNitbCtrl.SUBSCR_MODIFY_VAR, value)
data = ctrl.receive()
(answer, data) = ctrl.remove_ipa_ctrl_header(data)
answer_str = answer.decode('utf-8')
res = OsmoNitbCtrl.SUBSCR_MODIFY_REPLY_RE.match(answer_str)
if not res:
raise RuntimeError('Cannot create subscriber %r (answer=%r)' % (imsi, answer_str))
self.dbg('Created subscriber', imsi=imsi, msisdn=msisdn)
assert self.set_var('subscriber-modify-v1', value) == 'OK'
self.dbg('Created subscriber', imsi=imsi, msisdn=msisdn)
def subscriber_delete(self, imsi):
with self.ctrl() as ctrl:
ctrl.do_set(OsmoNitbCtrl.SUBSCR_DELETE_VAR, imsi)
data = ctrl.receive()
(answer, data) = ctrl.remove_ipa_ctrl_header(data)
answer_str = answer.decode('utf-8')
res = OsmoNitbCtrl.SUBSCR_DELETE_REPLY_RE.match(answer_str)
if not res:
raise RuntimeError('Cannot delete subscriber %r (answer=%r)' % (imsi, answer_str))
self.dbg('Deleted subscriber', imsi=imsi)
assert self.set_var('subscriber-delete-v1', imsi) == 'Removed'
self.dbg('Deleted subscriber', imsi=imsi)
def subscriber_list_active(self):
aslist_str = ""
with self.ctrl() as ctrl:
ctrl.do_get(OsmoNitbCtrl.SUBSCR_LIST_ACTIVE_VAR)
# This is legacy code from the old osmo-gsm-tester.
# looks like this doesn't work for long data.
data = ctrl.receive()
while (len(data) > 0):
(answer, data) = ctrl.remove_ipa_ctrl_header(data)
answer_str = answer.decode('utf-8')
answer_str = answer_str.replace('\n', ' ')
aslist_str = answer_str
return aslist_str
return self.get_var('subscriber-list-active-v1').replace('\n', ' ')
def bts_is_connected(self, bts_num):
with self.ctrl() as ctrl:
ctrl.do_get(OsmoNitbCtrl.BTS_OML_STATE_VAR % bts_num)
data = ctrl.receive()
while (len(data) > 0):
(answer, data) = ctrl.remove_ipa_ctrl_header(data)
answer_str = answer.decode('utf-8')
answer_str = answer_str.replace('\n', ' ')
res = OsmoNitbCtrl.BTS_OML_STATE_RE.match(answer_str)
if res:
oml_state = str(res.group('oml_state'))
if oml_state == 'connected':
return True
return False
return self.get_var('bts.%d.oml-connection-state' % bts_num) == 'connected'
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -20,8 +20,20 @@
import socket
import struct
import re
from ..core import log
from ..core.event_loop import MainLoop
VERB_SET = 'SET'
VERB_GET = 'GET'
VERB_SET_REPLY = 'SET_REPLY'
VERB_GET_REPLY = 'GET_REPLY'
VERB_TRAP = 'TRAP'
VERB_ERROR = 'ERROR'
RECV_VERBS = (VERB_GET_REPLY, VERB_SET_REPLY, VERB_TRAP, VERB_ERROR)
recv_re = re.compile('(%s) ([0-9]+) (.*)' % ('|'.join(RECV_VERBS)),
re.MULTILINE + re.DOTALL)
class CtrlInterfaceExn(Exception):
pass
@ -56,41 +68,168 @@ class OsmoCtrl(log.Origin):
raise CtrlInterfaceExn("Wrong protocol in answer!")
return data[4:plen+3], data[plen+3:]
def connect(self):
self.dbg('Connecting')
self.sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sck.connect((self.host, self.port))
def try_connect(self):
'''Do a connection attempt, return True when successful, False otherwise.
Does not raise exceptions, but logs them to the debug log.'''
assert self.sck is None
try:
self.dbg('Connecting')
sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sck.connect((self.host, self.port))
except:
sck.close()
raise
# set self.sck only after the connect was successful
self.sck = sck
return True
except:
self.dbg('Failed to connect', sys.exc_info()[0])
return False
def connect(self, timeout=30):
'''Connect to the CTRL self.host and self.port, retry for 'timeout' seconds.'''
MainLoop.wait(self.try_connect, timestep=3, timeout=timeout)
self.sck.setblocking(1)
self.sck.settimeout(10)
def disconnect(self):
if self.sck is None:
return
self.dbg('Disconnecting')
if self.sck is not None:
self.sck.close()
self.sck.close()
self.sck = None
def _send(self, data):
def _recv(self, verbs, match_args=None, match_id=None, attempts=10, length=1024):
'''Receive until a response matching the verbs / args / msg-id is obtained from CTRL.
The general socket timeout applies for each attempt made, see connect().
Multiple attempts may be necessary if, for example, intermediate
messages are received that do not relate to what is expected, like
TRAPs that are not interesting.
To receive a GET_REPLY / SET_REPLY:
verb, rx_id, val = _recv(('GET_REPLY', 'ERROR'), match_id=used_id)
if verb == 'ERROR':
raise CtrlInterfaceExn()
print(val)
To receive a TRAP:
verb, rx_id, val = _recv('TRAP', 'bts_connection_status connected')
# val == 'bts_connection_status connected'
If the CTRL is not connected yet, open and close a connection for
this operation only.
'''
# allow calling for both already connected VTY as well as establishing
# a connection just for this command.
if self.sck is None:
with self:
return self._recv(verbs, match_args=match_args,
match_id=match_id, attempts=attempts, length=length)
if isinstance(verbs, str):
verbs = (verbs, )
for i in range(attempts):
data = self.sck.recv(length)
self.dbg('Receiving', data=data)
while len(data) > 0:
msg, data = self.remove_ipa_ctrl_header(data)
msg_str = msg.decode('utf-8')
m = recv_re.fullmatch(msg_str)
if m is None:
raise CtrlInterfaceExn('Received garbage: %r' % data)
rx_verb, rx_id, rx_args = m.groups()
rx_id = int(rx_id)
if match_id is not None and match_id != rx_id:
continue
if verbs and rx_verb not in verbs:
continue
if match_args and not rx_args.startswith(match_args):
continue
return rx_verb, rx_id, rx_args
raise CtrlInterfaceExn('No answer found: ' + reply_header)
def _sendrecv(self, verb, send_args, *recv_args, use_id=None, **recv_kwargs):
'''Send a request and receive a matching response.
If the CTRL is not connected yet, open and close a connection for
this operation only.
'''
if self.sck is None:
with self:
return self._sendrecv(verb, send_args, *recv_args, use_id=use_id, **recv_kwargs)
if use_id is None:
use_id = self.next_id()
# send
data = '{verb} {use_id} {send_args}'.format(**locals())
self.dbg('Sending', data=data)
data = self.prefix_ipa_ctrl_header(data)
self.sck.send(data)
def receive(self, length = 1024):
data = self.sck.recv(length)
self.dbg('Receiving', data=data)
return data
# receive reply
recv_kwargs['match_id'] = use_id
return self._recv(*recv_args, **recv_kwargs)
def do_set(self, var, value, use_id=None):
if use_id is None:
use_id = self.next_id()
setmsg = "SET %s %s %s" %(use_id, var, value)
self._send(setmsg)
return use_id
def set_var(self, var, value):
'''Set the value of a specific variable on a CTRL interface, and return the response, e.g.:
assert set_var('subscriber-modify-v1', '901701234567,2342') == 'OK'
If the CTRL is not connected yet, open and close a connection for
this operation only.
'''
verb, rx_id, args = self._sendrecv(VERB_SET, '%s %s' % (var, value), (VERB_SET_REPLY, VERB_ERROR))
def do_get(self, var, use_id=None):
if use_id is None:
use_id = self.next_id()
getmsg = "GET %s %s" %(use_id, var)
self._send(getmsg)
return use_id
if verb == VERB_ERROR:
raise CtrlInterfaceExn('SET %s = %s returned %r' % (var, value, ' '.join((verb, str(rx_id), args))))
var_and_space = var + ' '
if not args.startswith(var_and_space):
raise CtrlInterfaceExn('SET %s = %s returned SET_REPLY for different var: %r'
% (var, value, ' '.join((verb, str(rx_id), args))))
return args[len(var_and_space):]
def get_var(self, var):
'''Get the value of a specific variable from a CTRL interface:
assert get_var('bts.0.oml-connection-state') == 'connected'
If the CTRL is not connected yet, open and close a connection for
this operation only.
'''
verb, rx_id, args = self._sendrecv(VERB_GET, var, (VERB_GET_REPLY, VERB_ERROR))
if verb == VERB_ERROR:
raise CtrlInterfaceExn('GET %s returned %r' % (var, ' '.join((verb, str(rx_id), args))))
var_and_space = var + ' '
if not args.startswith(var_and_space):
raise CtrlInterfaceExn('GET %s returned GET_REPLY for different var: %r'
% (var, value, ' '.join((verb, str(rx_id), args))))
return args[len(var_and_space):]
def get_int_var(self, var):
'''Same as get_var() but return an int'''
return int(self.get_var(var))
def get_trap(self, name):
'''Read from CTRL until a TRAP of this name is received.
If name is None, any TRAP is returned.
If the CTRL is not connected yet, open and close a connection for
this operation only.
'''
verb, rx_id, args = self._recv(VERB_TRAP, name)
name_and_space = var + ' '
# _recv() should ensure this:
assert args.startswith(name_and_space)
return args[len(name_and_space):]
def __enter__(self):
self.connect()