This is the example of the Application (Perl module) that provisions SIM card details to the HSS. The Application is subscribed to process event types of the Subcriber group.
#!/usr/bin/perl # Example Web Service to process events from EventSender handler # # run: # PORTA_BILLING_API=10.0.3.6 \ # PORTA_BILLING_API_USER=api-login \ # PORTA_BILLING_API_PASSWORD=api-password \ # RESULT_FILE=/tmp/hss.log \ # SERVICE_LOGIN=events \ # SERVICE_PASSWORD=topsecret \ # plackup --host 127.0.0.1 --port 9090 perl_example.psgi use strict; use warnings; use Const::Fast; use Cpanel::JSON::XS qw(decode_json encode_json); use English qw(-no_match_vars); use HTTP::Status qw(:constants); use HTTP::Tiny; use IO::File; use MIME::Base64 qw(encode_base64); use Plack::Request; use POSIX qw(strftime); use Cache::LRU; const my $RESULT_FILE => ( $ENV{RESULT_FILE} // '/tmp/hss.log' ); # basic authorization my $user = $ENV{SERVICE_LOGIN} // 'events'; my $password = $ENV{SERVICE_PASSWORD} // 'topsecret'; my $base_auth_string = 'Basic' . encode_base64( $user . ':' . $password, '' ); # PortaBilling API server my $PB_API_HOST = $ENV{PORTA_BILLING_API} // '10.0.0.1'; my $PB_API_USER = $ENV{PORTA_BILLING_API_USER} // ''; my $PB_API_PASSWORD = $ENV{PORTA_BILLING_API_PASSWORD} // ''; # reuse PB API session my $SESSION_EXPIRATION = $ENV{SESSION_EXPIRATION} // 60; my ( $session, $session_last_usage ); my $http = HTTP::Tiny->new( verify_SSL => 0, timeout => 5 ); # track active requests to detect retries my $active_req = Cache::LRU->new( size => 100 ); # error logging sub log_error { my $message = shift; print STDERR '[ERROR] ', $message, "\n"; return; } sub log_debug { my $message = shift; print STDERR '[DEBUG] ', $message, "\n"; return; } # Perform HTTP/REST request to PortaBilling API sub get_api_result { my ( $method, $session_id, $params ) = @_; log_debug( sprintf "API: POST https://%s/rest/%s %s", $PB_API_HOST, $method, encode_json($params) ); my $response = $http->post_form( 'https://' . $PB_API_HOST . '/rest/' . $method, { auth_info => encode_json( $session_id ? { session_id => $session_id } : { login => $PB_API_USER, password => $PB_API_PASSWORD, } ), params => encode_json($params), } ); if ( !$response->{success} ) { log_error( sprintf 'PB API %s failed, error %s %s', $method, $response->{status}, $response->{reason} ); return undef; } # debug, if required: #print STDERR 'PB API ', $method, ' response: ', # $response->{content}, "\n"; my $data = eval { decode_json( $response->{content} ) }; if ( $EVAL_ERROR || !$data ) { # no content or malformed JSON log_error( sprintf 'Failed to parse reply content: %s, error %s', $response->{content} // '', $EVAL_ERROR ); return undef; } return $data; } ## end sub get_api_result # Login to PortaBilling API sub api_login { my ( $api_login, $api_password ) = @_; if ( $session_last_usage && $session_last_usage + $SESSION_EXPIRATION > time() ) { # session active log_debug( sprintf 'Reusing session %s', $session ); return $session; } my $data = get_api_result( 'Session/login', undef, { login => $api_login, password => $api_password, } ); return undef if ( !$data ); $session = $data->{session_id}; $session_last_usage = time(); log_debug( sprintf 'Created session %s', $session ); return $session; } ## end sub api_login # Get Account information sub api_get_account_info { my ( $session_id, $i_account ) = @_; my $data = get_api_result( 'Account/get_account_info', $session_id, { i_account => $i_account } ); return undef if ( !$data ); $session_last_usage = time(); return $data->{account_info}; } # Get list of SIM Cards assigned to Account sub api_get_sim_cards { my ( $session_id, $i_account ) = @_; my $data = get_api_result( 'SIMCard/get_card_list', $session_id, { i_account => $i_account } ); return undef if ( !$data ); $session_last_usage = time(); return $data->{card_list}; } # Here we perform actual provisioning of collected data # to external HSS # As an example, we just write information to local file # row format: action,account-id,balance,status,IMSI,datetime # where # action - string, one of 'Created', 'Updated', 'Deleted' # account-id - string, ID of account # balance - number, account's balance # status - string, account's status # IMSI - string, SIM card IMSI (optional) # datetime - string, datetime in YYYY-MM-DD hh:mm:ss format sub provision_external_system { my $h = shift; my $status = 0; my $account = $h->{account}; my $sim_list = $h->{sim_cards} // []; my $datetime = strftime( '%Y-%m-%d %H:%M:%S', localtime() ); my $fh = IO::File->new( $RESULT_FILE, 'a' ); if ( !defined $fh ) { log_error( sprintf( 'Failed to open file %s, error %s', $RESULT_FILE, $OS_ERROR ) ); return $status; } if ( scalar( @{$sim_list} ) == 0 ) { # Account without SIM cards if ( !printf $fh "%s,%s,%.5f,%s,,%s\n", $h->{action}, $account->{id}, $account->{balance}, ( $account->{status} || 'open' ), $datetime ) { log_error( sprintf( 'Failed to write file %s, error %s', $RESULT_FILE, $OS_ERROR ) ); $status = 0; } $status = 1; } else { foreach my $sim ( @{$sim_list} ) { if ( !printf $fh "%s,%s,%.5f,%s,%s,%s\n", $h->{action}, $account->{id}, $account->{balance}, ( $account->{status} || 'open' ), $sim->{imsi}, $datetime ) { log_error( sprintf( 'Failed to write file %s, error %s', $RESULT_FILE, $OS_ERROR ) ); $status = 0; last; } $status = 1; } ## end foreach my $sim ( @{$sim_list...}) } ## end else [ if ( scalar( @{$sim_list...}))] if ( !$fh->close ) { log_error( sprintf( 'Failed to close file %s, error %s', $RESULT_FILE, $OS_ERROR ) ); $status = 0; } log_debug( sprintf 'Provisioning status: %s', ( $status ? 'OK' : 'FAILURE' ) ); # TEST: instert some random delay, # to emulate request timeout on remote side my $test_sleep = int( rand(10) ); log_debug( 'Emulate network delay, sleep ' . $test_sleep ); sleep($test_sleep); return $status; } ## end sub provision_external_system # check requirements for incoming request sub validate_request { my $req = shift; # HTTP method if ( $req->method ne 'POST' ) { log_error('Only POST method allowed'); return HTTP_METHOD_NOT_ALLOWED; } # Basic Authorization my $auth_value = $req->header('Authorization') || ''; if ( $auth_value ne $base_auth_string ) { log_error('Auth failed'); return HTTP_UNAUTHORIZED; } # require Content-Type: application/json if ( $req->content_type ne 'application/json' ) { log_error( sprintf 'Content-Type %s, expected application/json', $req->content_type ); return HTTP_UNSUPPORTED_MEDIA_TYPE; } return 0; } ## end sub validate_request sub process_request { my $req = shift; my $code = validate_request($req); return $code if ( $code > 0 ); # parse request my $event_content = $req->content; my $event = eval { decode_json($event_content) }; if ( $EVAL_ERROR || !$event ) { # received malformed JSON data: 400 Bad Request log_error('Malformed JSON request'); return HTTP_BAD_REQUEST; } log_debug( sprintf 'Received event: %s Variables: %s', $event->{event_type}, join( ' ', map { $_ . '=' . $event->{variables}->{$_} } ( sort keys %{ $event->{variables} } ) ) ); # detect retries for long-running requests my $unique_id = $event->{variables}->{i_event}; if ( defined $unique_id && ( my $cached_result = $active_req->get($unique_id) ) ) { log_debug( sprintf 'Detected retry request #%d, result: %s', $unique_id, $cached_result ); if ( $cached_result ne '-' ) { # remove stored result # and return it without 'long' processing $active_req->remove($unique_id); return $cached_result; } else { # request 'in-progress'. Depending on implementation # it can wait for result or start new processing. # For this example we restart processing $active_req->remove($unique_id); } } ## end if ( defined $unique_id...) # Subscriber/Created # Subscriber/Updated # Subscriber/Deleted # variables: i_account my ( $object, $action ) = split( /\//, $event->{event_type}, 2 ); if ( $object ne 'Subscriber' ) { # ignore return HTTP_OK; } my $i_account = $event->{variables}->{i_account}; if ( !$i_account ) { # mandatory variable missing: 400 Bad Request return HTTP_BAD_REQUEST; } my $api_session = api_login( $PB_API_USER, $PB_API_PASSWORD ); if ( !$api_session ) { log_error('PB API login failed'); return HTTP_INTERNAL_SERVER_ERROR; } my $account = api_get_account_info( $api_session, $i_account ); if ( !$account ) { log_error('Account not found'); return HTTP_OK; } my $sim_card_list = api_get_sim_cards( $api_session, $i_account ); if ( !$sim_card_list ) { log_error('Failed to get SIM Cards for Account'); return HTTP_INTERNAL_SERVER_ERROR; } # store 'start' of processing $active_req->set( $unique_id => '-' ); if ( !provision_external_system( { action => $action, account => $account, sim_cards => $sim_card_list, } ) ) { # TODO add required error processing (alerts, retries, etc) $active_req->remove($unique_id); return HTTP_INTERNAL_SERVER_ERROR; } # store result $active_req->set( $unique_id => HTTP_OK ); return HTTP_OK; } ## end sub process_request # PSGI application my $app = sub { my $env = shift; my $req = Plack::Request->new($env); my $code = process_request($req); return $req->new_response($code)->finalize; }; log_debug('Started'); return $app; |