Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 1 | |
| 2 | # osmo_gsm_tester: specifics for running a sysmoBTS |
| 3 | # |
| 4 | # Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH |
| 5 | # |
| 6 | # Author: Neels Hofmeyr <neels@hofmeyr.de> |
| 7 | # |
| 8 | # This program is free software: you can redistribute it and/or modify |
Harald Welte | 2720534 | 2017-06-03 09:51:45 +0200 | [diff] [blame] | 9 | # it under the terms of the GNU General Public License as |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 10 | # published by the Free Software Foundation, either version 3 of the |
| 11 | # License, or (at your option) any later version. |
| 12 | # |
| 13 | # This program is distributed in the hope that it will be useful, |
| 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
Harald Welte | 2720534 | 2017-06-03 09:51:45 +0200 | [diff] [blame] | 16 | # GNU General Public License for more details. |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 17 | # |
Harald Welte | 2720534 | 2017-06-03 09:51:45 +0200 | [diff] [blame] | 18 | # You should have received a copy of the GNU General Public License |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 20 | |
| 21 | import socket |
| 22 | import struct |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 23 | import re |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 24 | |
Pau Espin Pedrol | e1a58bd | 2020-04-10 20:46:07 +0200 | [diff] [blame] | 25 | from ..core import log |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 26 | from ..core.event_loop import MainLoop |
| 27 | |
| 28 | VERB_SET = 'SET' |
| 29 | VERB_GET = 'GET' |
| 30 | VERB_SET_REPLY = 'SET_REPLY' |
| 31 | VERB_GET_REPLY = 'GET_REPLY' |
| 32 | VERB_TRAP = 'TRAP' |
| 33 | VERB_ERROR = 'ERROR' |
| 34 | RECV_VERBS = (VERB_GET_REPLY, VERB_SET_REPLY, VERB_TRAP, VERB_ERROR) |
| 35 | recv_re = re.compile('(%s) ([0-9]+) (.*)' % ('|'.join(RECV_VERBS)), |
| 36 | re.MULTILINE + re.DOTALL) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 37 | |
| 38 | class CtrlInterfaceExn(Exception): |
| 39 | pass |
| 40 | |
| 41 | class OsmoCtrl(log.Origin): |
| 42 | |
| 43 | def __init__(self, host, port): |
Neels Hofmeyr | 1a7a3f0 | 2017-06-10 01:18:27 +0200 | [diff] [blame] | 44 | super().__init__(log.C_BUS, 'Ctrl', host=host, port=port) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 45 | self.host = host |
| 46 | self.port = port |
| 47 | self.sck = None |
Neels Hofmeyr | f79a86f | 2020-11-30 22:04:41 +0100 | [diff] [blame] | 48 | self._next_id = 0 |
| 49 | |
| 50 | def next_id(self): |
| 51 | ret = self._next_id |
| 52 | self._next_id += 1 |
| 53 | return ret |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 54 | |
| 55 | def prefix_ipa_ctrl_header(self, data): |
| 56 | if isinstance(data, str): |
| 57 | data = data.encode('utf-8') |
| 58 | s = struct.pack(">HBB", len(data)+1, 0xee, 0) |
| 59 | return s + data |
| 60 | |
| 61 | def remove_ipa_ctrl_header(self, data): |
| 62 | if (len(data) < 4): |
| 63 | raise CtrlInterfaceExn("Answer too short!") |
| 64 | (plen, ipa_proto, osmo_proto) = struct.unpack(">HBB", data[:4]) |
| 65 | if (plen + 3 > len(data)): |
| 66 | self.err('Warning: Wrong payload length', expected=plen, got=len(data)-3) |
| 67 | if (ipa_proto != 0xee or osmo_proto != 0): |
| 68 | raise CtrlInterfaceExn("Wrong protocol in answer!") |
| 69 | return data[4:plen+3], data[plen+3:] |
| 70 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 71 | def try_connect(self): |
| 72 | '''Do a connection attempt, return True when successful, False otherwise. |
| 73 | Does not raise exceptions, but logs them to the debug log.''' |
| 74 | assert self.sck is None |
| 75 | try: |
| 76 | self.dbg('Connecting') |
| 77 | sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 78 | try: |
| 79 | sck.connect((self.host, self.port)) |
| 80 | except: |
| 81 | sck.close() |
| 82 | raise |
| 83 | # set self.sck only after the connect was successful |
| 84 | self.sck = sck |
| 85 | return True |
| 86 | except: |
| 87 | self.dbg('Failed to connect', sys.exc_info()[0]) |
| 88 | return False |
| 89 | |
| 90 | def connect(self, timeout=30): |
| 91 | '''Connect to the CTRL self.host and self.port, retry for 'timeout' seconds.''' |
| 92 | MainLoop.wait(self.try_connect, timestep=3, timeout=timeout) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 93 | self.sck.setblocking(1) |
Neels Hofmeyr | 05439d7 | 2020-12-01 03:52:55 +0100 | [diff] [blame] | 94 | self.sck.settimeout(10) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 95 | |
| 96 | def disconnect(self): |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 97 | if self.sck is None: |
| 98 | return |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 99 | self.dbg('Disconnecting') |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 100 | self.sck.close() |
| 101 | self.sck = None |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 102 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 103 | def _recv(self, verbs, match_args=None, match_id=None, attempts=10, length=1024): |
| 104 | '''Receive until a response matching the verbs / args / msg-id is obtained from CTRL. |
| 105 | The general socket timeout applies for each attempt made, see connect(). |
| 106 | Multiple attempts may be necessary if, for example, intermediate |
| 107 | messages are received that do not relate to what is expected, like |
| 108 | TRAPs that are not interesting. |
| 109 | |
| 110 | To receive a GET_REPLY / SET_REPLY: |
| 111 | verb, rx_id, val = _recv(('GET_REPLY', 'ERROR'), match_id=used_id) |
| 112 | if verb == 'ERROR': |
| 113 | raise CtrlInterfaceExn() |
| 114 | print(val) |
| 115 | |
| 116 | To receive a TRAP: |
| 117 | verb, rx_id, val = _recv('TRAP', 'bts_connection_status connected') |
| 118 | # val == 'bts_connection_status connected' |
| 119 | |
| 120 | If the CTRL is not connected yet, open and close a connection for |
| 121 | this operation only. |
| 122 | ''' |
| 123 | |
| 124 | # allow calling for both already connected VTY as well as establishing |
| 125 | # a connection just for this command. |
| 126 | if self.sck is None: |
| 127 | with self: |
| 128 | return self._recv(verbs, match_args=match_args, |
| 129 | match_id=match_id, attempts=attempts, length=length) |
| 130 | |
| 131 | if isinstance(verbs, str): |
| 132 | verbs = (verbs, ) |
| 133 | |
| 134 | for i in range(attempts): |
| 135 | data = self.sck.recv(length) |
| 136 | self.dbg('Receiving', data=data) |
| 137 | while len(data) > 0: |
| 138 | msg, data = self.remove_ipa_ctrl_header(data) |
| 139 | msg_str = msg.decode('utf-8') |
| 140 | |
| 141 | m = recv_re.fullmatch(msg_str) |
| 142 | if m is None: |
| 143 | raise CtrlInterfaceExn('Received garbage: %r' % data) |
| 144 | |
| 145 | rx_verb, rx_id, rx_args = m.groups() |
| 146 | rx_id = int(rx_id) |
| 147 | |
| 148 | if match_id is not None and match_id != rx_id: |
| 149 | continue |
| 150 | |
| 151 | if verbs and rx_verb not in verbs: |
| 152 | continue |
| 153 | |
| 154 | if match_args and not rx_args.startswith(match_args): |
| 155 | continue |
| 156 | |
| 157 | return rx_verb, rx_id, rx_args |
| 158 | raise CtrlInterfaceExn('No answer found: ' + reply_header) |
| 159 | |
| 160 | def _sendrecv(self, verb, send_args, *recv_args, use_id=None, **recv_kwargs): |
| 161 | '''Send a request and receive a matching response. |
| 162 | If the CTRL is not connected yet, open and close a connection for |
| 163 | this operation only. |
| 164 | ''' |
| 165 | if self.sck is None: |
| 166 | with self: |
| 167 | return self._sendrecv(verb, send_args, *recv_args, use_id=use_id, **recv_kwargs) |
| 168 | |
| 169 | if use_id is None: |
| 170 | use_id = self.next_id() |
| 171 | |
| 172 | # send |
| 173 | data = '{verb} {use_id} {send_args}'.format(**locals()) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 174 | self.dbg('Sending', data=data) |
| 175 | data = self.prefix_ipa_ctrl_header(data) |
| 176 | self.sck.send(data) |
| 177 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 178 | # receive reply |
| 179 | recv_kwargs['match_id'] = use_id |
| 180 | return self._recv(*recv_args, **recv_kwargs) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 181 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 182 | def set_var(self, var, value): |
| 183 | '''Set the value of a specific variable on a CTRL interface, and return the response, e.g.: |
| 184 | assert set_var('subscriber-modify-v1', '901701234567,2342') == 'OK' |
| 185 | If the CTRL is not connected yet, open and close a connection for |
| 186 | this operation only. |
| 187 | ''' |
| 188 | verb, rx_id, args = self._sendrecv(VERB_SET, '%s %s' % (var, value), (VERB_SET_REPLY, VERB_ERROR)) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 189 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 190 | if verb == VERB_ERROR: |
| 191 | raise CtrlInterfaceExn('SET %s = %s returned %r' % (var, value, ' '.join((verb, str(rx_id), args)))) |
| 192 | |
| 193 | var_and_space = var + ' ' |
| 194 | if not args.startswith(var_and_space): |
| 195 | raise CtrlInterfaceExn('SET %s = %s returned SET_REPLY for different var: %r' |
| 196 | % (var, value, ' '.join((verb, str(rx_id), args)))) |
| 197 | |
| 198 | return args[len(var_and_space):] |
| 199 | |
| 200 | def get_var(self, var): |
| 201 | '''Get the value of a specific variable from a CTRL interface: |
| 202 | assert get_var('bts.0.oml-connection-state') == 'connected' |
| 203 | If the CTRL is not connected yet, open and close a connection for |
| 204 | this operation only. |
| 205 | ''' |
| 206 | verb, rx_id, args = self._sendrecv(VERB_GET, var, (VERB_GET_REPLY, VERB_ERROR)) |
| 207 | |
| 208 | if verb == VERB_ERROR: |
| 209 | raise CtrlInterfaceExn('GET %s returned %r' % (var, ' '.join((verb, str(rx_id), args)))) |
| 210 | |
| 211 | var_and_space = var + ' ' |
| 212 | if not args.startswith(var_and_space): |
| 213 | raise CtrlInterfaceExn('GET %s returned GET_REPLY for different var: %r' |
| 214 | % (var, value, ' '.join((verb, str(rx_id), args)))) |
| 215 | |
| 216 | return args[len(var_and_space):] |
| 217 | |
| 218 | def get_int_var(self, var): |
| 219 | '''Same as get_var() but return an int''' |
| 220 | return int(self.get_var(var)) |
| 221 | |
| 222 | def get_trap(self, name): |
| 223 | '''Read from CTRL until a TRAP of this name is received. |
| 224 | If name is None, any TRAP is returned. |
| 225 | If the CTRL is not connected yet, open and close a connection for |
| 226 | this operation only. |
| 227 | ''' |
| 228 | verb, rx_id, args = self._recv(VERB_TRAP, name) |
| 229 | name_and_space = var + ' ' |
| 230 | # _recv() should ensure this: |
| 231 | assert args.startswith(name_and_space) |
| 232 | return args[len(name_and_space):] |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 233 | |
| 234 | def __enter__(self): |
| 235 | self.connect() |
| 236 | return self |
| 237 | |
| 238 | def __exit__(self, *exc_info): |
| 239 | self.disconnect() |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 240 | |
| 241 | # vim: expandtab tabstop=4 shiftwidth=4 |