blob: 4d0a023f9a5f0e3b7231c1a17f4d440dff25b395 [file] [log] [blame]
Max93118812017-02-21 13:34:55 +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
25__version__ = "v0.7" # bump this on every non-trivial change
26
27from twisted.internet import defer, reactor
28from twisted_ipa import CTRL, IPAFactory, __version__ as twisted_ipa_version
29from ipa import Ctrl
30from treq import post, collect
31from suds.client import Client
32from functools import partial
33from distutils.version import StrictVersion as V # FIXME: use NormalizedVersion from PEP-386 when available
34import argparse, datetime, signal, sys, os, logging, logging.handlers
35
36# we don't support older versions of TwistedIPA module
37assert V(twisted_ipa_version) > V('0.4')
38
39# keys from OpenBSC openbsc/src/libbsc/bsc_rf_ctrl.c, values SOAP-specific
40oper = { 'inoperational' : 0, 'operational' : 1 }
41admin = { 'locked' : 0, 'unlocked' : 1 }
42policy = { 'off' : 0, 'on' : 1, 'grace' : 2, 'unknown' : 3 }
43
44# keys from OpenBSC openbsc/src/libbsc/bsc_vty.c
45fix = { 'invalid' : 0, 'fix2d' : 1, 'fix3d' : 1 } # SOAP server treats it as boolean but expects int
46
47
48def handle_reply(p, f, log, r):
49 """
50 Reply handler: takes function p to process raw SOAP server reply r, function f to run for each command and verbosity flag v
51 """
52 repl = p(r) # result is expected to have both commands[] array and error string (could be None)
53 bsc_id = repl.commands[0].split()[0].split('.')[3] # we expect 1st command to have net.0.bsc.666.bts.2.trx.1 location prefix format
54 log.info("Received SOAP response for BSC %s with %d commands, error status: %s" % (bsc_id, len(repl.commands), repl.error))
55 log.debug("BSC %s commands: %s" % (bsc_id, repl.commands))
56 for t in repl.commands: # Process OpenBscCommands format from .wsdl
57 (_, m) = Ctrl().cmd(*t.split())
58 f(m)
59
60
61class Trap(CTRL):
62 """
63 TRAP handler (agnostic to factory's client object)
64 """
65 def ctrl_TRAP(self, data, op_id, v):
66 """
67 Parse CTRL TRAP and dispatch to appropriate handler after normalization
68 """
69 (l, r) = v.split()
70 loc = l.split('.')
71 t_type = loc[-1]
72 p = partial(lambda a, i: a[i] if len(a) > i else None, loc) # parse helper
73 method = getattr(self, 'handle_' + t_type.replace('-', ''), lambda: "Unhandled %s trap" % t_type)
74 method(p(1), p(3), p(5), p(7), r) # we expect net.0.bsc.666.bts.2.trx.1 format for trap prefix
75
76 def ctrl_SET_REPLY(self, data, _, v):
77 """
78 Debug log for replies to our commands
79 """
80 self.factory.log.debug('SET REPLY %s' % v)
81
82 def ctrl_ERROR(self, data, op_id, v):
83 """
84 We want to know if smth went wrong
85 """
86 self.factory.log.debug('CTRL ERROR [%s] %s' % (op_id, v))
87
88 def connectionMade(self):
89 """
90 Logging wrapper, calling super() is necessary not to break reconnection logic
91 """
92 self.factory.log.info("Connected to CTRL@%s:%d" % (self.factory.host, self.factory.port))
93 super(CTRL, self).connectionMade()
94
95 @defer.inlineCallbacks
96 def handle_locationstate(self, net, bsc, bts, trx, data):
97 """
98 Handle location-state TRAP: parse trap content, build SOAP context and use treq's routines to post it while setting up async handlers
99 """
100 (ts, fx, lat, lon, height, opr, adm, pol, mcc, mnc) = data.split(',')
101 tstamp = datetime.datetime.fromtimestamp(float(ts)).isoformat()
102 self.factory.log.debug('location-state@%s.%s.%s.%s (%s) [%s/%s] => %s' % (net, bsc, bts, trx, tstamp, mcc, mnc, data))
103 ctx = self.factory.client.registerSiteLocation(bsc, float(lon), float(lat), fix.get(fx, 0), tstamp, oper.get(opr, 2), admin.get(adm, 2), policy.get(pol, 3))
104 d = post(self.factory.location, ctx.envelope)
105 d.addCallback(collect, partial(handle_reply, ctx.process_reply, self.transport.write, self.factory.log)) # treq's collect helper is handy to get all reply content at once using closure on ctx
106 d.addErrback(lambda e, bsc: self.factory.log.critical("HTTP POST error %s while trying to register BSC %s" % (e, bsc)), bsc) # handle HTTP errors
107 # Ensure that we run only limited number of requests in parallel:
108 yield self.factory.semaphore.acquire()
109 yield d # we end up here only if semaphore is available which means it's ok to fire the request without exceeding the limit
110 self.factory.semaphore.release()
111
112 def handle_notificationrejectionv1(self, net, bsc, bts, trx, data):
113 """
114 Handle notification-rejection-v1 TRAP: just an example to show how more message types can be handled
115 """
116 self.factory.log.debug('notification-rejection-v1@bsc-id %s => %s' % (bsc, data))
117
118
119class TrapFactory(IPAFactory):
120 """
121 Store SOAP client object so TRAP handler can use it for requests
122 """
123 location = None
124 log = None
125 semaphore = None
126 client = None
127 host = None
128 port = None
129 def __init__(self, host, port, proto, semaphore, log, wsdl=None, location=None):
130 self.host = host # for logging only,
131 self.port = port # seems to be no way to get it from ReconnectingClientFactory
132 self.log = log
133 self.semaphore = semaphore
134 soap = Client(wsdl, location=location, nosend=True) # make async SOAP client
135 self.location = location.encode() if location else soap.wsdl.services[0].ports[0].location # necessary for dispatching HTTP POST via treq
136 self.client = soap.service
137 level = self.log.getEffectiveLevel()
138 self.log.setLevel(logging.WARNING) # we do not need excessive debug from lower levels
139 super(TrapFactory, self).__init__(proto, self.log)
140 self.log.setLevel(level)
141 self.log.debug("Using IPA %s, SUDS client: %s" % (Ctrl.version, soap))
142
143
144def reloader(path, script, log, dbg1, dbg2, signum, _):
145 """
146 Signal handler: we have to use execl() because twisted's reactor is not restartable due to some bug in twisted implementation
147 """
148 log.info("Received Signal %d - restarting..." % signum)
149 if signum == signal.SIGUSR1 and dbg1 not in sys.argv and dbg2 not in sys.argv:
150 sys.argv.append(dbg1) # enforce debug
151 if signum == signal.SIGUSR2 and (dbg1 in sys.argv or dbg2 in sys.argv): # disable debug
152 if dbg1 in sys.argv:
153 sys.argv.remove(dbg1)
154 if dbg2 in sys.argv:
155 sys.argv.remove(dbg2)
156 os.execl(path, script, *sys.argv[1:])
157
158
159if __name__ == '__main__':
160 p = argparse.ArgumentParser(description='Proxy between given SOAP service and Osmocom CTRL protocol.')
161 p.add_argument('-v', '--version', action='version', version=("%(prog)s " + __version__))
162 p.add_argument('-p', '--port', type=int, default=4250, help="Port to use for CTRL interface, defaults to 4250")
163 p.add_argument('-c', '--ctrl', default='localhost', help="Adress to use for CTRL interface, defaults to localhost")
164 p.add_argument('-w', '--wsdl', required=True, help="WSDL URL for SOAP")
165 p.add_argument('-n', '--num', type=int, default=5, help="Max number of concurrent HTTP requests to SOAP server")
166 p.add_argument('-d', '--debug', action='store_true', help="Enable debug log")
167 p.add_argument('-o', '--output', action='store_true', help="Log to STDOUT in addition to SYSLOG")
168 p.add_argument('-l', '--location', help="Override location found in WSDL file (don't use unless you know what you're doing)")
169 args = p.parse_args()
170
171 log = logging.getLogger('CTRL2SOAP')
172 if args.debug:
173 log.setLevel(logging.DEBUG)
174 else:
175 log.setLevel(logging.INFO)
176 log.addHandler(logging.handlers.SysLogHandler('/dev/log'))
177 if args.output:
178 log.addHandler(logging.StreamHandler(sys.stdout))
179
180 reboot = partial(reloader, os.path.abspath(__file__), os.path.basename(__file__), log, '-d', '--debug') # keep in sync with add_argument() call above
181 signal.signal(signal.SIGHUP, reboot)
182 signal.signal(signal.SIGQUIT, reboot)
183 signal.signal(signal.SIGUSR1, reboot) # restart and enabled debug output
184 signal.signal(signal.SIGUSR2, reboot) # restart and disable debug output
185
186 log.info("SOAP proxy %s starting with PID %d ..." % (__version__, os.getpid()))
187 reactor.connectTCP(args.ctrl, args.port, TrapFactory(args.ctrl, args.port, Trap, defer.DeferredSemaphore(args.num), log, args.wsdl, args.location))
188 reactor.run()