blob: 644025f5b9d5ef6490711dff275b1ae2cac3d319 [file] [log] [blame]
Neels Hofmeyr3531a192017-03-28 14:30:28 +02001
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 Welte27205342017-06-03 09:51:45 +02009# it under the terms of the GNU General Public License as
Neels Hofmeyr3531a192017-03-28 14:30:28 +020010# 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 Welte27205342017-06-03 09:51:45 +020016# GNU General Public License for more details.
Neels Hofmeyr3531a192017-03-28 14:30:28 +020017#
Harald Welte27205342017-06-03 09:51:45 +020018# You should have received a copy of the GNU General Public License
Neels Hofmeyr3531a192017-03-28 14:30:28 +020019# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21import socket
22import struct
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +010023import re
Neels Hofmeyr3531a192017-03-28 14:30:28 +020024
Pau Espin Pedrole1a58bd2020-04-10 20:46:07 +020025from ..core import log
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +010026from ..core.event_loop import MainLoop
27
28VERB_SET = 'SET'
29VERB_GET = 'GET'
30VERB_SET_REPLY = 'SET_REPLY'
31VERB_GET_REPLY = 'GET_REPLY'
32VERB_TRAP = 'TRAP'
33VERB_ERROR = 'ERROR'
34RECV_VERBS = (VERB_GET_REPLY, VERB_SET_REPLY, VERB_TRAP, VERB_ERROR)
35recv_re = re.compile('(%s) ([0-9]+) (.*)' % ('|'.join(RECV_VERBS)),
36 re.MULTILINE + re.DOTALL)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020037
38class CtrlInterfaceExn(Exception):
39 pass
40
41class OsmoCtrl(log.Origin):
42
43 def __init__(self, host, port):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020044 super().__init__(log.C_BUS, 'Ctrl', host=host, port=port)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020045 self.host = host
46 self.port = port
47 self.sck = None
Neels Hofmeyrf79a86f2020-11-30 22:04:41 +010048 self._next_id = 0
49
50 def next_id(self):
51 ret = self._next_id
52 self._next_id += 1
53 return ret
Neels Hofmeyr3531a192017-03-28 14:30:28 +020054
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 Hofmeyr5b04ef22020-12-01 04:48:38 +010071 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 Hofmeyr3531a192017-03-28 14:30:28 +020093 self.sck.setblocking(1)
Neels Hofmeyr05439d72020-12-01 03:52:55 +010094 self.sck.settimeout(10)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020095
96 def disconnect(self):
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +010097 if self.sck is None:
98 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099 self.dbg('Disconnecting')
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +0100100 self.sck.close()
101 self.sck = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200102
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +0100103 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 Hofmeyr3531a192017-03-28 14:30:28 +0200174 self.dbg('Sending', data=data)
175 data = self.prefix_ipa_ctrl_header(data)
176 self.sck.send(data)
177
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +0100178 # receive reply
179 recv_kwargs['match_id'] = use_id
180 return self._recv(*recv_args, **recv_kwargs)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200181
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +0100182 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 Hofmeyr3531a192017-03-28 14:30:28 +0200189
Neels Hofmeyr5b04ef22020-12-01 04:48:38 +0100190 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 Hofmeyr3531a192017-03-28 14:30:28 +0200233
234 def __enter__(self):
235 self.connect()
236 return self
237
238 def __exit__(self, *exc_info):
239 self.disconnect()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200240
241# vim: expandtab tabstop=4 shiftwidth=4