2018-07-11 12:05:13 +00:00
#!/usr/bin/python3
# -*- mode: python-mode; py-indent-tabs-mode: nil -*-
"""
/ *
* Copyright ( C ) 2018 sysmocom s . f . m . c . GmbH
*
* All Rights Reserved
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License along
* with this program ; if not , write to the Free Software Foundation , Inc . ,
* 51 Franklin Street , Fifth Floor , Boston , MA 02110 - 1301 USA .
* /
"""
2018-12-05 17:07:04 +00:00
__version__ = " 0.0.8 " # bump this on every non-trivial change
2018-07-11 12:05:13 +00:00
2018-12-05 16:58:40 +00:00
import argparse , os , logging , logging . handlers , datetime
2018-07-11 12:05:13 +00:00
import hashlib
import json
import configparser
2018-11-28 10:55:19 +00:00
from functools import partial
from distutils . version import StrictVersion as V # FIXME: use NormalizedVersion from PEP-386 when available
from twisted . internet import defer , reactor
from treq import post , collect
from osmopy . trap_helper import debug_init , get_type , get_r , p_h , make_params , comm_proc
from osmopy . twisted_ipa import CTRL , IPAFactory , __version__ as twisted_ipa_version
from osmopy . osmo_ipa import Ctrl
2018-07-11 12:05:13 +00:00
# we don't support older versions of TwistedIPA module
assert V ( twisted_ipa_version ) > V ( ' 0.4 ' )
2018-12-05 16:58:40 +00:00
def handle_reply ( ts , bid , f , log , resp ) :
2018-07-11 12:05:13 +00:00
"""
Reply handler : process raw CGI server response , function f to run for each command
"""
2018-11-27 16:43:45 +00:00
decoded = json . loads ( resp . decode ( ' utf-8 ' ) )
2018-12-05 16:58:40 +00:00
log . debug ( ' request for BSC %s took %d seconds ' % ( bid , ( datetime . datetime . now ( ) - ts ) . total_seconds ( ) ) )
2018-12-05 16:49:36 +00:00
comm_proc ( decoded . get ( ' commands ' ) , bid , f , log )
2018-07-11 12:05:13 +00:00
def gen_hash ( params , skey ) :
2018-11-28 10:55:19 +00:00
inp = ' '
for key in [ ' time_stamp ' , ' position_validity ' , ' admin_status ' , ' policy_status ' ] :
inp + = str ( params . get ( key ) )
inp + = skey
for key in [ ' bsc_id ' , ' lat ' , ' lon ' , ' position_validity ' ] :
inp + = str ( params . get ( key ) )
2018-07-11 12:05:13 +00:00
m = hashlib . md5 ( )
2018-11-28 10:55:19 +00:00
m . update ( inp . encode ( ' utf-8 ' ) )
2018-07-11 12:05:13 +00:00
res = m . hexdigest ( )
#print('HASH: \nparams="%r"\ninput="%s" \nres="%s"' %(params, input, res))
return res
2018-12-05 17:07:04 +00:00
def make_async_req ( ts , dst , par , f_write , f_log , tout ) :
d = post ( dst , par , timeout = tout )
2018-12-05 16:58:40 +00:00
d . addCallback ( collect , partial ( handle_reply , ts , par [ ' bsc_id ' ] , f_write , f_log ) ) # treq's collect helper is handy to get all reply content at once
2018-12-05 17:07:04 +00:00
d . addErrback ( lambda e : f_log . critical ( " HTTP POST error %s while trying to register BSC %s on %s (timeout %d ) " % ( e , par [ ' bsc_id ' ] , dst , tout ) ) ) # handle HTTP errors
2018-12-05 15:03:57 +00:00
return d
2018-11-27 16:42:07 +00:00
class Trap ( CTRL ) :
"""
TRAP handler ( agnostic to factory ' s client object)
"""
def ctrl_TRAP ( self , data , op_id , v ) :
"""
Parse CTRL TRAP and dispatch to appropriate handler after normalization
"""
self . factory . log . debug ( ' TRAP %s ' % v )
t_type = get_type ( v )
p = p_h ( v )
method = getattr ( self , ' handle_ ' + t_type . replace ( ' - ' , ' ' ) , lambda * _ : " Unhandled %s trap " % t_type )
method ( p ( 1 ) , p ( 3 ) , p ( 5 ) , p ( 7 ) , get_r ( v ) )
def ctrl_SET_REPLY ( self , data , _ , v ) :
"""
Debug log for replies to our commands
"""
self . factory . log . debug ( ' SET REPLY %s ' % v )
def ctrl_ERROR ( self , data , op_id , v ) :
"""
We want to know if smth went wrong
"""
self . factory . log . debug ( ' CTRL ERROR [ %s ] %s ' % ( op_id , v ) )
def connectionMade ( self ) :
"""
Logging wrapper , calling super ( ) is necessary not to break reconnection logic
"""
2018-12-05 15:00:29 +00:00
self . factory . log . info ( " Connected to CTRL@ %s : %d " % ( self . factory . addr_ctrl , self . factory . port_ctrl ) )
2018-11-27 16:42:07 +00:00
super ( CTRL , self ) . connectionMade ( )
def handle_locationstate ( self , net , bsc , bts , trx , data ) :
"""
Handle location - state TRAP : parse trap content , build CGI Request and use treq ' s routines to post it while setting up async handlers
"""
params = make_params ( bsc , data )
2018-12-05 16:55:58 +00:00
self . factory . log . info ( ' location-state@ %s . %s . %s . %s ( %s ) => %s ' % ( net , bsc , bts , trx , params [ ' time_stamp ' ] , data ) )
2018-11-27 16:42:07 +00:00
params [ ' h ' ] = gen_hash ( params , self . factory . secret_key )
2018-12-05 16:58:40 +00:00
t = datetime . datetime . now ( )
self . factory . log . debug ( ' Preparing request for BSC %s @ %s ... ' % ( params [ ' bsc_id ' ] , t ) )
2018-11-27 16:42:07 +00:00
# Ensure that we run only limited number of requests in parallel:
2018-12-05 17:07:04 +00:00
self . factory . semaphore . run ( make_async_req , t , self . factory . location , params , self . transport . write , self . factory . log , self . factory . timeout )
2018-11-27 16:42:07 +00:00
def handle_notificationrejectionv1 ( self , net , bsc , bts , trx , data ) :
"""
Handle notification - rejection - v1 TRAP : just an example to show how more message types can be handled
"""
self . factory . log . debug ( ' notification-rejection-v1@bsc-id %s => %s ' % ( bsc , data ) )
2018-07-11 12:05:13 +00:00
class TrapFactory ( IPAFactory ) :
"""
Store CGI information so TRAP handler can use it for requests
"""
2018-12-05 15:00:29 +00:00
def __init__ ( self , proto , log ) :
2018-07-11 12:05:13 +00:00
self . log = log
level = self . log . getEffectiveLevel ( )
self . log . setLevel ( logging . WARNING ) # we do not need excessive debug from lower levels
super ( TrapFactory , self ) . __init__ ( proto , self . log )
self . log . setLevel ( level )
2018-12-05 15:00:29 +00:00
self . log . debug ( " Using Osmocom IPA library v %s " % Ctrl . version )
2018-07-11 12:05:13 +00:00
if __name__ == ' __main__ ' :
p = argparse . ArgumentParser ( description = ' Proxy between given GCI service and Osmocom CTRL protocol. ' )
p . add_argument ( ' -v ' , ' --version ' , action = ' version ' , version = ( " %(prog)s v " + __version__ ) )
2018-11-26 15:42:59 +00:00
p . add_argument ( ' -d ' , ' --debug ' , action = ' store_true ' , help = " Enable debug log " ) # keep in sync with debug_init call below
2018-12-05 15:00:29 +00:00
p . add_argument ( ' -c ' , ' --config-file ' , required = True , help = " Path to mandatory config file (in INI format). " )
args = p . parse_args ( namespace = TrapFactory )
2018-07-11 12:05:13 +00:00
2018-12-05 12:49:13 +00:00
log = debug_init ( ' CTRL2CGI ' , args . debug )
2018-07-11 12:05:13 +00:00
2018-12-05 15:00:29 +00:00
T = TrapFactory ( Trap , log )
config = configparser . ConfigParser ( interpolation = None )
config . read ( args . config_file )
T . addr_ctrl = config [ ' main ' ] . get ( ' addr_ctrl ' , ' localhost ' )
T . port_ctrl = config [ ' main ' ] . getint ( ' port_ctrl ' , 4250 )
2018-12-05 17:07:04 +00:00
T . timeout = config [ ' main ' ] . getint ( ' timeout ' , 30 )
2018-12-05 15:00:29 +00:00
T . semaphore = defer . DeferredSemaphore ( config [ ' main ' ] . getint ( ' num_max_conn ' , 5 ) )
T . location = config [ ' main ' ] . get ( ' location ' )
T . secret_key = config [ ' main ' ] . get ( ' secret_key ' )
log . info ( " CGI proxy v %s starting with PID %d : " % ( __version__ , os . getpid ( ) ) )
log . info ( " destination %s (concurrency %d ) " % ( T . location , T . semaphore . limit ) )
log . info ( " connecting to %s : %d ... " % ( T . addr_ctrl , T . port_ctrl ) )
reactor . connectTCP ( T . addr_ctrl , T . port_ctrl , T )
2018-07-11 12:05:13 +00:00
reactor . run ( )