blob: 8d0ef9fffdcd656ec62f773170c73a515ea85d82 [file] [log] [blame]
Max68823132016-11-15 19:10:05 +01001#!/usr/bin/python3
2# -*- mode: python-mode; py-indent-tabs-mode: nil -*-
3"""
4/*
5 * Copyright (C) 2016 sysmocom s.f.m.c. GmbH
6 *
7 * All Rights Reserved
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 */
23"""
24
25from ipa import Ctrl, IPA
26from twisted.internet.protocol import ReconnectingClientFactory
27from twisted.internet import reactor
28from twisted.protocols import basic
29import argparse
30
31class IPACommon(basic.Int16StringReceiver):
32 """
33 Generic IPA protocol handler: include some routines for simpler subprotocols.
34 It's not intended as full implementation of all subprotocols, rather common ground and example code.
35 """
36 def dbg(self, line):
37 """
38 Debug print helper
39 """
40 if self.factory.debug:
41 print(line)
42
43 def osmo_CTRL(self, data):
44 """
45 OSMO CTRL protocol
46 Placeholder, see corresponding derived class
47 """
48 pass
49
50 def osmo_MGCP(self, data):
51 """
52 OSMO MGCP extension
53 """
54 self.dbg('OSMO MGCP received %s' % data)
55
56 def osmo_LAC(self, data):
57 """
58 OSMO LAC extension
59 """
60 self.dbg('OSMO LAC received %s' % data)
61
62 def osmo_SMSC(self, data):
63 """
64 OSMO SMSC extension
65 """
66 self.dbg('OSMO SMSC received %s' % data)
67
68 def osmo_ORC(self, data):
69 """
70 OSMO ORC extension
71 """
72 self.dbg('OSMO ORC received %s' % data)
73
74 def osmo_GSUP(self, data):
75 """
76 OSMO GSUP extension
77 """
78 self.dbg('OSMO GSUP received %s' % data)
79
80 def osmo_OAP(self, data):
81 """
82 OSMO OAP extension
83 """
84 self.dbg('OSMO OAP received %s' % data)
85
86 def osmo_UNKNOWN(self, data):
87 """
88 OSMO defaul extension handler
89 """
90 self.dbg('OSMO unknown extension received %s' % data)
91
92 def handle_RSL(self, data, proto, extension):
93 """
94 RSL protocol handler
95 """
96 self.dbg('IPA RSL received message with extension %s' % extension)
97
98 def handle_CCM(self, data, proto, msgt):
99 """
100 CCM (IPA Connection Management)
101 Placeholder, see corresponding derived class
102 """
103 pass
104
105 def handle_SCCP(self, data, proto, extension):
106 """
107 SCCP protocol handler
108 """
109 self.dbg('IPA SCCP received message with extension %s' % extension)
110
111 def handle_OML(self, data, proto, extension):
112 """
113 OML protocol handler
114 """
115 self.dbg('IPA OML received message with extension %s' % extension)
116
117 def handle_OSMO(self, data, proto, extension):
118 """
119 Dispatcher point for OSMO subprotocols based on extension name, lambda default should never happen
120 """
121 method = getattr(self, 'osmo_' + IPA().ext(extension), lambda: "extension dispatch failure")
122 method(data)
123
124 def handle_MGCP(self, data, proto, extension):
125 """
126 MGCP protocol handler
127 """
128 self.dbg('IPA MGCP received message with attribute %s' % extension)
129
130 def handle_UNKNOWN(self, data, proto, extension):
131 """
132 Default protocol handler
133 """
134 self.dbg('IPA received message for %s (%s) protocol with attribute %s' % (IPA().proto(proto), proto, extension))
135
136 def process_chunk(self, data):
137 """
138 Generic message dispatcher for IPA (sub)protocols based on protocol name, lambda default should never happen
139 """
140 (_, proto, extension, content) = IPA().del_header(data)
141 if content is not None:
142 self.dbg('IPA received %s::%s [%d/%d] %s' % (IPA().proto(proto), IPA().ext_name(proto, extension), len(data), len(content), content))
143 method = getattr(self, 'handle_' + IPA().proto(proto), lambda: "protocol dispatch failure")
144 method(content, proto, extension)
145
146 def dataReceived(self, data):
147 """
148 Override for dataReceived from Int16StringReceiver because of inherently incompatible interpretation of length
149 If default handler is used than we would always get off-by-1 error (Int16StringReceiver use equivalent of l + 2)
150 """
151 if len(data):
152 (head, tail) = IPA().split_combined(data)
153 self.process_chunk(head)
154 self.dataReceived(tail)
155
156 def connectionMade(self):
157 """
158 We have to resetDelay() here to drop internal state to default values to make reconnection logic work
159 Make sure to call this via super() if overriding to keep reconnection logic intact
160 """
161 self.dbg('IPA connection made!')
162 self.factory.resetDelay()
163
164
165class CCM(IPACommon):
166 """
167 Implementation of CCM protocol for IPA multiplex
168 """
169 def ack(self):
170 self.transport.write(IPA().id_ack())
171
172 def ping(self):
173 self.transport.write(IPA().ping())
174
175 def pong(self):
176 self.transport.write(IPA().pong())
177
178 def handle_CCM(self, data, proto, msgt):
179 """
180 CCM (IPA Connection Management)
181 Only basic logic necessary for tests is implemented (ping-pong, id ack etc)
182 """
183 if msgt == IPA.MSGT['ID_GET']:
184 self.transport.getHandle().sendall(IPA().id_resp(self.factory.ccm_id))
185 # if we call
186 # self.transport.write(IPA().id_resp(self.factory.test_id))
187 # instead, than we would have to also call
188 # reactor.callLater(1, self.ack)
189 # instead of self.ack()
190 # otherwise the writes will be glued together - hence the necessity for ugly hack with 1s timeout
191 # Note: this still might work depending on the IPA implementation details on the other side
192 self.ack()
193 # schedule PING in 4s
194 reactor.callLater(4, self.ping)
195 if msgt == IPA.MSGT['PING']:
196 self.pong()
197
198
199class CTRL(IPACommon):
200 """
201 Implementation of Osmocom control protocol for IPA multiplex
202 """
203 def ctrl_SET(self, data, op_id, v):
204 """
205 Handle CTRL SET command
206 """
207 self.dbg('CTRL SET [%s] %s' % (op_id, v))
208
209 def ctrl_SET_REPLY(self, data, op_id, v):
210 """
211 Handle CTRL SET reply
212 """
213 self.dbg('CTRL SET REPLY [%s] %s' % (op_id, v))
214
215 def ctrl_GET(self, data, op_id, v):
216 """
217 Handle CTRL GET command
218 """
219 self.dbg('CTRL GET [%s] %s' % (op_id, v))
220
221 def ctrl_GET_REPLY(self, data, op_id, v):
222 """
223 Handle CTRL GET reply
224 """
225 self.dbg('CTRL GET REPLY [%s] %s' % (op_id, v))
226
227 def ctrl_TRAP(self, data, op_id, v):
228 """
229 Handle CTRL TRAP command
230 """
231 self.dbg('CTRL TRAP [%s] %s' % (op_id, v))
232
233 def ctrl_ERROR(self, data, op_id, v):
234 """
235 Handle CTRL ERROR reply
236 """
237 self.dbg('CTRL ERROR [%s] %s' % (op_id, v))
238
239 def osmo_CTRL(self, data):
240 """
241 OSMO CTRL message dispatcher, lambda default should never happen
242 For basic tests only, appropriate handling routines should be replaced: see CtrlServer for example
243 """
244 self.dbg('OSMO CTRL received %s::%s' % Ctrl().parse(data.decode('utf-8')))
245 (cmd, op_id, v) = data.decode('utf-8').split(' ', 2)
246 method = getattr(self, 'ctrl_' + cmd, lambda: "CTRL unknown command")
247 method(data, op_id, v)
248
249
250class IPAServer(CCM):
251 """
252 Test implementation of IPA server
253 Demonstrate CCM opearation by overriding necessary bits from CCM
254 """
255 def connectionMade(self):
256 """
257 Keep reconnection logic working by calling routine from CCM
258 Initiate CCM upon connection
259 """
260 print('IPA server connection made!')
261 super(IPAServer, self).connectionMade()
262 self.transport.write(IPA().id_get())
263
264
265class CtrlServer(CTRL):
266 """
267 Test implementation of CTRL server
268 Demonstarte CTRL handling by overriding simpler routines from CTRL
269 """
270 def connectionMade(self):
271 """
272 Keep reconnection logic working by calling routine from CTRL
273 Send TRAP upon connection
274 Note: we can't use sendString() because of it's incompatibility with IPA interpretation of length prefix
275 """
276 print('CTRL server connection made!')
277 super(CtrlServer, self).connectionMade()
278 self.transport.write(Ctrl().trap('LOL', 'what'))
279 self.transport.write(Ctrl().trap('rulez', 'XXX'))
280
281 def reply(self, r):
282 self.transport.write(Ctrl().add_header(r))
283
284 def ctrl_SET(self, data, op_id, v):
285 """
286 CTRL SET command: always succeed
287 """
288 print('SET [%s] %s' % (op_id, v))
289 self.reply('SET_REPLY %s %s' % (op_id, v))
290
291 def ctrl_GET(self, data, op_id, v):
292 """
293 CTRL GET command: always fail
294 """
295 print('GET [%s] %s' % (op_id, v))
296 self.reply('ERROR %s No variable found' % op_id)
297
298
299class IPAFactory(ReconnectingClientFactory):
300 """
301 Generic IPA Client Factory which can be used to store state for various subprotocols and manage connections
302 Note: so far we do not really need separate Factory for acting as a server due to protocol simplicity
303 """
304 protocol = IPACommon
305 debug = False
306 ccm_id = IPA().identity(unit=b'1515/0/1', mac=b'b0:0b:fa:ce:de:ad:be:ef', utype=b'sysmoBTS', name=b'StingRay', location=b'hell', sw=IPA.version.encode('utf-8'))
307
308 def __init__(self, proto=None, debug=False, ccm_id=None):
309 if proto:
310 self.protocol = proto
311 if debug:
312 self.debug = debug
313 if ccm_id:
314 self.ccm_id = ccm_id
315
316 def clientConnectionFailed(self, connector, reason):
317 """
318 Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method
319 """
320 if self.debug:
321 print('IPAFactory connection failed:', reason.getErrorMessage())
322 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
323
324 def clientConnectionLost(self, connector, reason):
325 """
326 Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method
327 """
328 if self.debug:
329 print('IPAFactory connection lost:', reason.getErrorMessage())
330 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
331
332
333if __name__ == '__main__':
334 p = argparse.ArgumentParser("Twisted IPA (module v%s) app" % IPA.version)
335 p.add_argument('-v', '--version', action='version', version='%(prog)s v0.3')
336 p.add_argument('-p', '--port', type=int, default=4250, help="Port to use for CTRL interface")
337 p.add_argument('-d', '--host', default='localhost', help="Adress to use for CTRL interface")
338 cs = p.add_mutually_exclusive_group()
339 cs.add_argument("-c", "--client", action='store_true', help="asume client role")
340 cs.add_argument("-s", "--server", action='store_true', help="asume server role")
341 ic = p.add_mutually_exclusive_group()
342 ic.add_argument("--ipa", action='store_true', help="use IPA protocol")
343 ic.add_argument("--ctrl", action='store_true', help="use CTRL protocol")
344 args = p.parse_args()
345 test = False
346 if args.ctrl:
347 if args.client:
348 # Start osmo-bsc to receive TRAP messages when osmo-bts-* connects to it
349 print('CTRL client, connecting to %s:%d' % (args.host, args.port))
350 reactor.connectTCP(args.host, args.port, IPAFactory(CTRL, debug=True))
351 test = True
352 if args.server:
353 # Use bsc_control.py to issue set/get commands
354 print('CTRL server, listening on port %d' % args.port)
355 reactor.listenTCP(args.port, IPAFactory(CtrlServer, debug=True))
356 test = True
357 if args.ipa:
358 if args.client:
359 # Start osmo-nitb which would initiate A-bis/IP session
360 print('IPA client, connecting to %s ports %d and %d' % (args.host, IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL))
361 reactor.connectTCP(args.host, IPA.TCP_PORT_OML, IPAFactory(CCM, debug=True))
362 reactor.connectTCP(args.host, IPA.TCP_PORT_RSL, IPAFactory(CCM, debug=True))
363 test = True
364 if args.server:
365 # Start osmo-bts-* which would attempt to connect to us
366 print('IPA server, listening on ports %d and %d' % (IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL))
367 reactor.listenTCP(IPA.TCP_PORT_RSL, IPAFactory(IPAServer, debug=True))
368 reactor.listenTCP(IPA.TCP_PORT_OML, IPAFactory(IPAServer, debug=True))
369 test = True
370 if test:
371 reactor.run()
372 else:
373 print("Please specify which protocol in which role you'd like to test.")