blob: e6d7b1a167bbf98b18e3f78971ffb6cdcf7d284c [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
Max2d921622017-04-04 18:48:24 +020025__version__ = "0.6" # bump this on every non-trivial change
Maxca2778c2017-03-20 17:26:16 +010026
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
Maxdd22a302017-03-20 17:30:25 +010031import argparse, logging
Max68823132016-11-15 19:10:05 +010032
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 """
Maxdd22a302017-03-20 17:30:25 +010042 self.factory.log.debug(line)
Max68823132016-11-15 19:10:05 +010043
44 def osmo_CTRL(self, data):
45 """
46 OSMO CTRL protocol
47 Placeholder, see corresponding derived class
48 """
49 pass
50
51 def osmo_MGCP(self, data):
52 """
53 OSMO MGCP extension
54 """
55 self.dbg('OSMO MGCP received %s' % data)
56
57 def osmo_LAC(self, data):
58 """
59 OSMO LAC extension
60 """
61 self.dbg('OSMO LAC received %s' % data)
62
63 def osmo_SMSC(self, data):
64 """
65 OSMO SMSC extension
66 """
67 self.dbg('OSMO SMSC received %s' % data)
68
69 def osmo_ORC(self, data):
70 """
71 OSMO ORC extension
72 """
73 self.dbg('OSMO ORC received %s' % data)
74
75 def osmo_GSUP(self, data):
76 """
77 OSMO GSUP extension
78 """
79 self.dbg('OSMO GSUP received %s' % data)
80
81 def osmo_OAP(self, data):
82 """
83 OSMO OAP extension
84 """
85 self.dbg('OSMO OAP received %s' % data)
86
87 def osmo_UNKNOWN(self, data):
88 """
89 OSMO defaul extension handler
90 """
91 self.dbg('OSMO unknown extension received %s' % data)
92
93 def handle_RSL(self, data, proto, extension):
94 """
95 RSL protocol handler
96 """
97 self.dbg('IPA RSL received message with extension %s' % extension)
98
99 def handle_CCM(self, data, proto, msgt):
100 """
101 CCM (IPA Connection Management)
102 Placeholder, see corresponding derived class
103 """
104 pass
105
106 def handle_SCCP(self, data, proto, extension):
107 """
108 SCCP protocol handler
109 """
110 self.dbg('IPA SCCP received message with extension %s' % extension)
111
112 def handle_OML(self, data, proto, extension):
113 """
114 OML protocol handler
115 """
116 self.dbg('IPA OML received message with extension %s' % extension)
117
118 def handle_OSMO(self, data, proto, extension):
119 """
120 Dispatcher point for OSMO subprotocols based on extension name, lambda default should never happen
121 """
122 method = getattr(self, 'osmo_' + IPA().ext(extension), lambda: "extension dispatch failure")
123 method(data)
124
125 def handle_MGCP(self, data, proto, extension):
126 """
127 MGCP protocol handler
128 """
129 self.dbg('IPA MGCP received message with attribute %s' % extension)
130
131 def handle_UNKNOWN(self, data, proto, extension):
132 """
133 Default protocol handler
134 """
135 self.dbg('IPA received message for %s (%s) protocol with attribute %s' % (IPA().proto(proto), proto, extension))
136
137 def process_chunk(self, data):
138 """
139 Generic message dispatcher for IPA (sub)protocols based on protocol name, lambda default should never happen
140 """
141 (_, proto, extension, content) = IPA().del_header(data)
142 if content is not None:
143 self.dbg('IPA received %s::%s [%d/%d] %s' % (IPA().proto(proto), IPA().ext_name(proto, extension), len(data), len(content), content))
144 method = getattr(self, 'handle_' + IPA().proto(proto), lambda: "protocol dispatch failure")
145 method(content, proto, extension)
146
147 def dataReceived(self, data):
148 """
149 Override for dataReceived from Int16StringReceiver because of inherently incompatible interpretation of length
150 If default handler is used than we would always get off-by-1 error (Int16StringReceiver use equivalent of l + 2)
151 """
152 if len(data):
153 (head, tail) = IPA().split_combined(data)
154 self.process_chunk(head)
155 self.dataReceived(tail)
156
157 def connectionMade(self):
158 """
159 We have to resetDelay() here to drop internal state to default values to make reconnection logic work
160 Make sure to call this via super() if overriding to keep reconnection logic intact
161 """
Maxdd22a302017-03-20 17:30:25 +0100162 addr = self.transport.getPeer()
163 self.dbg('IPA connected to %s:%d peer' % (addr.host, addr.port))
Max68823132016-11-15 19:10:05 +0100164 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 """
Maxdd22a302017-03-20 17:30:25 +0100262 addr = self.transport.getPeer()
263 self.factory.log.info('IPA server: connection from %s:%d client' % (addr.host, addr.port))
Max68823132016-11-15 19:10:05 +0100264 super(IPAServer, self).connectionMade()
265 self.transport.write(IPA().id_get())
266
267
268class CtrlServer(CTRL):
269 """
270 Test implementation of CTRL server
271 Demonstarte CTRL handling by overriding simpler routines from CTRL
272 """
273 def connectionMade(self):
274 """
275 Keep reconnection logic working by calling routine from CTRL
276 Send TRAP upon connection
277 Note: we can't use sendString() because of it's incompatibility with IPA interpretation of length prefix
278 """
Maxdd22a302017-03-20 17:30:25 +0100279 addr = self.transport.getPeer()
280 self.factory.log.info('CTRL server: connection from %s:%d client' % (addr.host, addr.port))
Max68823132016-11-15 19:10:05 +0100281 super(CtrlServer, self).connectionMade()
282 self.transport.write(Ctrl().trap('LOL', 'what'))
283 self.transport.write(Ctrl().trap('rulez', 'XXX'))
284
285 def reply(self, r):
286 self.transport.write(Ctrl().add_header(r))
287
288 def ctrl_SET(self, data, op_id, v):
289 """
290 CTRL SET command: always succeed
291 """
Maxdd22a302017-03-20 17:30:25 +0100292 self.dbg('SET [%s] %s' % (op_id, v))
Max68823132016-11-15 19:10:05 +0100293 self.reply('SET_REPLY %s %s' % (op_id, v))
294
295 def ctrl_GET(self, data, op_id, v):
296 """
297 CTRL GET command: always fail
298 """
Maxdd22a302017-03-20 17:30:25 +0100299 self.dbg('GET [%s] %s' % (op_id, v))
Max68823132016-11-15 19:10:05 +0100300 self.reply('ERROR %s No variable found' % op_id)
301
302
303class IPAFactory(ReconnectingClientFactory):
304 """
305 Generic IPA Client Factory which can be used to store state for various subprotocols and manage connections
306 Note: so far we do not really need separate Factory for acting as a server due to protocol simplicity
307 """
308 protocol = IPACommon
Maxdd22a302017-03-20 17:30:25 +0100309 log = None
Max68823132016-11-15 19:10:05 +0100310 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'))
311
Maxdd22a302017-03-20 17:30:25 +0100312 def __init__(self, proto=None, log=None, ccm_id=None):
Max68823132016-11-15 19:10:05 +0100313 if proto:
314 self.protocol = proto
Max68823132016-11-15 19:10:05 +0100315 if ccm_id:
316 self.ccm_id = ccm_id
Maxdd22a302017-03-20 17:30:25 +0100317 if log:
318 self.log = log
319 else:
320 self.log = logging.getLogger('IPAFactory')
Max2d921622017-04-04 18:48:24 +0200321 self.log.setLevel(logging.CRITICAL)
322 self.log.addHandler(logging.NullHandler)
Max68823132016-11-15 19:10:05 +0100323
324 def clientConnectionFailed(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 """
Maxdd22a302017-03-20 17:30:25 +0100328 self.log.warning('IPAFactory connection failed: %s' % reason.getErrorMessage())
Max68823132016-11-15 19:10:05 +0100329 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
330
331 def clientConnectionLost(self, connector, reason):
332 """
333 Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method
334 """
Maxdd22a302017-03-20 17:30:25 +0100335 self.log.warning('IPAFactory connection lost: %s' % reason.getErrorMessage())
Max68823132016-11-15 19:10:05 +0100336 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
337
338
339if __name__ == '__main__':
340 p = argparse.ArgumentParser("Twisted IPA (module v%s) app" % IPA.version)
Maxca2778c2017-03-20 17:26:16 +0100341 p.add_argument('-v', '--version', action='version', version="%(prog)s v" + __version__)
Max68823132016-11-15 19:10:05 +0100342 p.add_argument('-p', '--port', type=int, default=4250, help="Port to use for CTRL interface")
343 p.add_argument('-d', '--host', default='localhost', help="Adress to use for CTRL interface")
344 cs = p.add_mutually_exclusive_group()
345 cs.add_argument("-c", "--client", action='store_true', help="asume client role")
346 cs.add_argument("-s", "--server", action='store_true', help="asume server role")
347 ic = p.add_mutually_exclusive_group()
348 ic.add_argument("--ipa", action='store_true', help="use IPA protocol")
349 ic.add_argument("--ctrl", action='store_true', help="use CTRL protocol")
350 args = p.parse_args()
351 test = False
Maxdd22a302017-03-20 17:30:25 +0100352
353 log = logging.getLogger('TwistedIPA')
354 log.setLevel(logging.DEBUG)
355 log.addHandler(logging.StreamHandler(sys.stdout))
356
Max68823132016-11-15 19:10:05 +0100357 if args.ctrl:
358 if args.client:
359 # Start osmo-bsc to receive TRAP messages when osmo-bts-* connects to it
360 print('CTRL client, connecting to %s:%d' % (args.host, args.port))
Maxdd22a302017-03-20 17:30:25 +0100361 reactor.connectTCP(args.host, args.port, IPAFactory(CTRL, log))
Max68823132016-11-15 19:10:05 +0100362 test = True
363 if args.server:
364 # Use bsc_control.py to issue set/get commands
365 print('CTRL server, listening on port %d' % args.port)
Maxdd22a302017-03-20 17:30:25 +0100366 reactor.listenTCP(args.port, IPAFactory(CtrlServer, log))
Max68823132016-11-15 19:10:05 +0100367 test = True
368 if args.ipa:
369 if args.client:
370 # Start osmo-nitb which would initiate A-bis/IP session
371 print('IPA client, connecting to %s ports %d and %d' % (args.host, IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL))
Maxdd22a302017-03-20 17:30:25 +0100372 reactor.connectTCP(args.host, IPA.TCP_PORT_OML, IPAFactory(CCM, log))
373 reactor.connectTCP(args.host, IPA.TCP_PORT_RSL, IPAFactory(CCM, log))
Max68823132016-11-15 19:10:05 +0100374 test = True
375 if args.server:
376 # Start osmo-bts-* which would attempt to connect to us
377 print('IPA server, listening on ports %d and %d' % (IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL))
Maxdd22a302017-03-20 17:30:25 +0100378 reactor.listenTCP(IPA.TCP_PORT_RSL, IPAFactory(IPAServer, log))
379 reactor.listenTCP(IPA.TCP_PORT_OML, IPAFactory(IPAServer, log))
Max68823132016-11-15 19:10:05 +0100380 test = True
381 if test:
382 reactor.run()
383 else:
384 print("Please specify which protocol in which role you'd like to test.")