APPENDIX B. Sample Application to process provisioning events

Link copied to clipboard

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;

On this page

Release
What's new
Admin manuals
Handbooks
Developers documentation
UI help