2021-04-04 18:55:24 +00:00
#!/usr/bin/env python3
2018-07-11 12:05:13 +00:00
# -*- 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 .
* /
"""
2019-01-07 14:53:35 +00:00
__version__ = " 0.1.1 " # 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
2018-12-21 17:42:30 +00:00
from distutils . version import StrictVersion as V
2018-11-28 10:55:19 +00:00
from twisted . internet import defer , reactor
from treq import post , collect
2019-01-07 14:53:35 +00:00
from osmopy . trap_helper import debug_init , get_type , get_r , p_h , gen_hash , make_params , comm_proc
2018-11-28 10:55:19 +00:00
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-21 12:36:36 +00:00
def log_duration ( log , bid , ts , ts_http ) :
"""
Log human - readable duration from timestamps
"""
base = datetime . datetime . now ( )
delta_t = datetime . timedelta ( seconds = ( base - ts ) . total_seconds ( ) )
delta_h = datetime . timedelta ( seconds = ( base - ts_http ) . total_seconds ( ) )
delta_w = delta_t - delta_h
log . debug ( ' Request for BSC %s took %s total ( %s wait, %s http) ' % ( bid , delta_t , delta_w , delta_h ) )
2018-07-11 12:05:13 +00:00
2018-12-21 12:36:36 +00:00
def handle_reply ( ts , ts_http , 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-21 12:36:36 +00:00
log_duration ( log , bid , ts , ts_http )
2018-12-05 16:49:36 +00:00
comm_proc ( decoded . get ( ' commands ' ) , bid , f , log )
2018-07-11 12:05:13 +00:00
2018-12-05 17:07:04 +00:00
def make_async_req ( ts , dst , par , f_write , f_log , tout ) :
2018-12-21 17:42:30 +00:00
"""
Assemble deferred request parameters and partially instantiate response handler
"""
2018-12-05 17:07:04 +00:00
d = post ( dst , par , timeout = tout )
2018-12-21 12:36:36 +00:00
d . addCallback ( collect , partial ( handle_reply , ts , datetime . datetime . now ( ) , par [ ' bsc_id ' ] , f_write , f_log ) )
2018-12-05 17:18:38 +00:00
d . addErrback ( lambda e : f_log . critical ( " HTTP POST error %s while trying to register BSC %s on %s (timeout %d ) " % ( repr ( 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
"""
2018-12-21 16:48:20 +00:00
if get_type ( v ) == ' location-state ' :
p = p_h ( v )
self . handle_locationstate ( p ( 1 ) , p ( 3 ) , p ( 5 ) , p ( 7 ) , get_r ( v ) )
else :
self . factory . log . debug ( ' Ignoring TRAP %s ' % ( v . split ( ) [ 0 ] ) )
2018-11-27 16:42:07 +00:00
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
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 ( )