Let’s have a look on how the Application works.
A service provider provisions subscriber details to their HSS when a mobile user is added to, modified (e.g., a phone number/SIM card/product is changed, etc.) or removed from PortaBilling.
In our example we assume that HSS requires SIM card details such as MSISDN, IMSI and a profile name that corresponds to the LTE service name.
Example 1. A mobile account is created in PortaBilling
- PortaBilling sends the POST request with Subscriber/Created event type and the i_account to the Application.
Date: Fri, 11 May 2018 13:28:08 GMT
Authorization: Signature keyId="test",algorithm="hmac-sha1",signature="b+Y3I1ymQTsuq0h3HNiIl3P3SdE="
Host: 192.168.243.244:5000
Referrer: http://192.168.243.244:5000/
TE: trailers
Content-Length: 83
Content-Type: application/json
{
"event_type": "Subscriber/Created",
"variables": {
"i_account": 1000889,
"i_event": "5"
}
}
- The Application receives the request.
- The Application sends a POST request to PortaBilling to establish an API session.
Used parameters:
params={"login":"demo","password":"exAmple"}
POST /rest/Session/login HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 70
params=%7B%22login%22%3A%22demo%22%2C%22password%22%3A%22exAmple%22%7D
- The Application receives the response from PortaBilling with the session_id.
- The Application takes the i_account and the session_id values and calls PortaBilling API to retrieve subscriber details such as service (e.g., LTE) and SIM card details (e.g., MSISDN, IMSI). The API methods are:
- Account/get_account_info to get the list of included services and ensure that the LTE service is enabled for this subscriber.
Used parameters:
auth_info={"session_id":"527865ee75368ff2d2c4f4881"}
params={"i_account":1000889,"get_included_services":1}
POST /rest/Account/get_account_info HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 139
auth_info=%7B%22session_id%22%3A%22527865ee75368ff2d2c4f4881%22%7D¶ms=% 7B%22i_account%22%3A1000889%2C%22get_included_services%22%3A1%7D
- SIMCard/get_card_list to get the MSISDN and IMSI.
Used parameters:
auth_info={"session_id":"527865ee75368ff2d2c4f4881"}
params={"i_account":1000889}
POST /rest/SIMCard/get_card_list HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 105
auth_info=%7B%22session_id%22%3A%22527865ee75368ff2d2c4f4881%22%7D¶ms=% 7B%22i_account%22%3A1000889%7D
- Account/get_account_info to get the list of included services and ensure that the LTE service is enabled for this subscriber.
Used parameters:
auth_info={"session_id":"527865ee75368ff2d2c4f4881"}
params={"i_account":1000889,"get_included_services":1}
Once the subscriber’s information is received, the Application interacts with the HSS via the HSS API to add a new subscriber with the following parameters:
- MSISDN: 12065551122
- IMSI: 310019901000045
- Profile name: LTE
Once the subscriber is provisioned, the Application replies to PortaBilling with 200 OK status code.
The ESPF removes the event from the event queue.
Example 2: The existing subscriber has been updated (a new SIM card is assigned)
- PortaBilling sends the POST request with the Subscriber/Updated event type and the i_account to the Application.
Date: Fri, 21 May 2018 13:28:08 GMT
Authorization: Signature keyId="test",algorithm="hmac-sha1",signature="b+Y3I1ymQTsuq0h3HNiIl3P3SdE="
Host: 192.168.243.244:5000
Referrer: http://192.168.243.244:5000/
TE: trailers
Content-Length: 83
Content-Type: application/json
{
"event_type": "Subscriber/Updated",
"variables": {
"i_account": "1000889"
"i_event": "6"
}
}
- The Application receives the request.
- The Application verifies that the API session is active and reuses the session ID for the request. Othervise, the application establishes a new API session.
- The Application uses the i_account and the session_id to call the following PortaBilling API methods:
- Account/get_account_info to get the list of included services, account status and account balance.
Used parameters:
auth_info={"session_id":"527865ee75368ff2d2c4f4881"}
params={"i_account":1000889,"get_included_services":1}
POST /rest/Account/get_account_info HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 139
auth_info=%7B%22session_id%22%3A%22527865ee75368ff2d2c4f4881%22%7D¶ms=% 7B%22i_account%22%3A1000889%2C%22get_included_services%22%3A1%7D
- SIMCard/get_card_list to get the MSISDN and IMSI.
Used parameters:
auth_info={"session_id":"527865ee75368ff2d2c4f4881"}
params={"i_account":1000889}
POST /rest/SIMCard/get_card_list HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 105
auth_info=%7B%22session_id%22%3A%22527865ee75368ff2d2c4f4881%22%7D¶ms=% 7B%22i_account%22%3A1000889%7D
- Account/get_account_info to get the list of included services, account status and account balance.
Used parameters:
auth_info={"session_id":"527865ee75368ff2d2c4f4881"}
params={"i_account":1000889,"get_included_services":1}
- Upon the response from PortaBilling, the Application requests the SIM card details from HSS via its API.
- The Application compares the SIM card parameters received from PortaBilling (MSISDN: 12065551122, IMSI: 310685901111133) with the ones received from the HSS (MSISDN: 12065551122, IMSI: 310685900000045).
- The Application detects that the IMSI has changed from 310685900000045 to 310685901111133 and notifies the HSS to delete a subscriber with the IMSI: 310685900000045.
- The Application then instructs the HSS to add a new subscriber with the following parameters:
- MSISDN: 12065551122
- IMSI: 310685901111133
- Profile name: LTE
If the Application detects that the account’s status has changed to blocked or suspended, it notifies the HSS to block a SIM card. If the Application detects that the account has no available funds or has reached the credit limit, it notifies the HSS to act respectively. Note that the actions here depend on the requirements of the HSS.
- Once the HSS is updated, the Application responds to PortaBilling with 200 OK status code.
- The ESPF removes the event from the event queue.
Example 3: The existing subscriber has been terminated
- PortaBilling sends the POST request with Subscriber/Deleted event type and the i_account to the Application
Date: Fri, 11 May 2018 13:28:08 GMT
Authorization: Signature keyId="test",algorithm="hmac-sha1",signature="b+Y3I1ymQTsuq0h3HNiIl3P3SdE="
Host: 192.168.243.244:5000
Referrer: http://192.168.243.244:5000/
TE: trailers
Content-Length: 83
Content-Type: application/json
{
"event_type": "Subscriber/Deleted",
"variables": {
"i_account": "1000889"
"i_event": "8"
}
}
- The Application verifies that the API session is active and reuses the session ID for the request. Othervise, the application establishes a new API session.
- The Application uses the i_account and the session_id to call the following API methods:
- Account/get_account_info to get the MSISDN (e.g., account ID).
Used parameters:
auth_info={"session_id":"999865ee75368ff2d2c4f4881"}
params={"i_account":1000889}
POST /rest/Account/get_account_info HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 105
auth_info=%7B%22session_id%22%3A%22999865ee75368ff2d2c4f4881%22%7D¶ms=% 7B%22i_account%22%3A1000889%7D
- SIMCard/get_card_list method to verify that the sim card is no longer assigned to the account.
Used parameters:
auth_info={"session_id":"999865ee75368ff2d2c4f4881"}
params={"i_account":1000889}
POST /rest/SIMCard/get_card_list HTTP/1.1
Host: demo.portaone.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 105
auth_info=%7B%22session_id%22%3A%22999865ee75368ff2d2c4f4881%22%7D¶ms=% 7B%22i_account%22%3A1000889%7D
- Account/get_account_info to get the MSISDN (e.g., account ID).
Used parameters:
auth_info={"session_id":"999865ee75368ff2d2c4f4881"}
params={"i_account":1000889}
- Upon the response from PortaBilling, the Application notifies the HSS to remove/terminate a subscriber with 12065551122 MSISDN.
- Once the SIM card is removed from the HSS, the Application replies to PortaBilling with 200 OK status code.
- The ESPF removes the event from the event queue.
Sample Application to process provisioning events
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; |