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