neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-gsm-tester/+/21564 )
Change subject: OsmoCtrl cleanup: get_var(), set_var(), get_trap() ...................................................................... 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 --- M src/osmo_gsm_tester/obj/bsc_osmo.py M src/osmo_gsm_tester/obj/msc_osmo.py M src/osmo_gsm_tester/obj/nitb_osmo.py M src/osmo_gsm_tester/obj/osmo_ctrl.py 4 files changed, 174 insertions(+), 127 deletions(-) git pull ssh://gerrit.osmocom.org:29418/osmo-gsm-tester refs/changes/64/21564/1 diff --git a/src/osmo_gsm_tester/obj/bsc_osmo.py b/src/osmo_gsm_tester/obj/bsc_osmo.py index cf54205..a16ef92 100644 --- a/src/osmo_gsm_tester/obj/bsc_osmo.py +++ b/src/osmo_gsm_tester/obj/bsc_osmo.py @@ -145,41 +145,27 @@ # 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() + def ctrl(self): + return OsmoBscCtrl(self) + def vty(self): return OsmoBscVty(self) -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): +class OsmoBscCtrl(osmo_ctrl.OsmoCtrl): + def __init__(self, bsc, port=4249): 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) + 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): PORT = 4242 diff --git a/src/osmo_gsm_tester/obj/msc_osmo.py b/src/osmo_gsm_tester/obj/msc_osmo.py index 67e1d31..5a7c0ba 100644 --- a/src/osmo_gsm_tester/obj/msc_osmo.py +++ b/src/osmo_gsm_tester/obj/msc_osmo.py @@ -157,29 +157,12 @@ 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 diff --git a/src/osmo_gsm_tester/obj/nitb_osmo.py b/src/osmo_gsm_tester/obj/nitb_osmo.py index a424927..ea00a75 100644 --- a/src/osmo_gsm_tester/obj/nitb_osmo.py +++ b/src/osmo_gsm_tester/obj/nitb_osmo.py @@ -158,22 +158,10 @@ 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 @@ 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 diff --git a/src/osmo_gsm_tester/obj/osmo_ctrl.py b/src/osmo_gsm_tester/obj/osmo_ctrl.py index c2dd7e3..89faef3 100644 --- a/src/osmo_gsm_tester/obj/osmo_ctrl.py +++ b/src/osmo_gsm_tester/obj/osmo_ctrl.py @@ -20,9 +20,20 @@ import socket import struct +import re from ..core import log +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 @@ -57,40 +68,156 @@ return data[4:plen+3], data[plen+3:] def connect(self): + assert self.sck is None self.dbg('Connecting') - self.sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sck.connect((self.host, self.port)) - self.sck.setblocking(1) - self.sck.settimeout(10) + sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sck.connect((self.host, self.port)) + try: + sck.setblocking(1) + sck.settimeout(10) + except: + sck.close() + raise + # set self.sck only after everything was successful + self.sck = sck 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() -- To view, visit https://gerrit.osmocom.org/c/osmo-gsm-tester/+/21564 To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings Gerrit-Project: osmo-gsm-tester Gerrit-Branch: master Gerrit-Change-Id: Id561e5a55d8057a997a8ec9e7fa6f94840194df1 Gerrit-Change-Number: 21564 Gerrit-PatchSet: 1 Gerrit-Owner: neels <nhofm...@sysmocom.de> Gerrit-MessageType: newchange