#!/usr/bin/perl use strict; use warnings; use English; use Getopt::Long; use Pod::Usage; use NGCP::API::Client; use Readonly; use XML::Simple; use Template; use Email::Sender::Simple qw(); use Email::Simple; use Email::Simple::Creator; use Email::Sender::Transport::Sendmail qw(); use POSIX qw(strftime); use File::Pid; Readonly my @required => qw(); Readonly my $config_file => '/etc/ngcp-panel/provisioning.conf'; my $PROGRAM_BASE = 'ngcp-fraud-notifier'; my $retcode = 0; my $piddir = '/run/ngcp-fraud-notifier'; my $pidfile = "$piddir/ngcp-fraud-notifier.pid"; my $pf = File::Pid->new({ file => $pidfile }); local $PROGRAM_NAME = $PROGRAM_BASE; local $OUTPUT_AUTOFLUSH = 1; local $SIG{INT} = \&cleanup; my $page_size = 100; my $opts = { verbose => 0, }; my $config; GetOptions($opts, 'help|h' => sub { usage() }, 'verbose', ) or usage(); sub DESTROY { cleanup(); } sub check_params { my @missing; foreach my $param (@required) { push @missing, $param unless $opts->{$param}; } usage(join(' ', @missing)) if scalar @missing; return; } sub usage { my $missing = shift; pod2usage( -exitval => $missing ? 1 : 0, -verbose => 1, -message => $missing ? "Missing parameters: $missing" : '', ); return; } sub load_config { $config = XML::Simple->new()->XMLin($config_file, ForceArray => 0) or die "Cannot load config: $config_file: $ERRNO\n"; return; } sub get_data { my ($uri, $link, $code) = @_; my $client = NGCP::API::Client->new( verbose => $opts->{verbose}, page_rows => $page_size, ); my @result = (); while (my $res = $client->next_page($uri)) { return [] unless check_api_error($res); my $res_hash = $res->as_hash; my $data = $res_hash->{_embedded}{'ngcp:' . $link}; if ('ARRAY' eq ref $data) { push @result, @{$data}; } elsif ($data) { push @result, $data; } if (defined $code and 'CODE' eq ref $code) { $code->(\@result); @result = (); } } return \@result; } sub check_api_error { my $res = shift; if ($res->is_success) { return 1; } else { die($res->result . "\n") if ( $res->is_client_error or ($res->is_server_error and not scalar grep { $_ == $res->code; } qw(502 503 504)) ); return 0; } } sub get_email_template { my $event = shift; my $lock_type = $event->{interval_lock} ? 'lock' : 'warning'; my $reseller_id = $event->{reseller_id}; my @templates_data = (); foreach my $reseller ($event->{reseller_id}, 'NULL') { @templates_data = ( @templates_data, @{get_data(sprintf('/api/emailtemplates/?reseller_id=%s', $reseller), 'emailtemplates')}); } my $selected_template; foreach my $template (@templates_data) { next if $template->{name} ne 'customer_fraud_' . $lock_type . '_default_email' && $template->{name} ne 'customer_fraud_' . $lock_type . '_email'; next if $template->{reseller_id} && $template->{reseller_id} != $reseller_id; $selected_template = $template; last if $template->{reseller_id}; } unless ($selected_template) { die sprintf "No template 'customer_fraud_%s_default_email' OR 'customer_fraud_%s_email' is defined.\n", $lock_type, $lock_type; } return $selected_template; } sub send_email { my ($event, $subscribers) = @_; my $template = get_email_template($event); my $vars = { adminmail => $config->{adminmail}, customer_id => $event->{contract_id}, interval => $event->{interval}, interval_cost => sprintf('%.2f', $event->{interval_cost} / 100), interval_limit => sprintf('%.2f', $event->{interval_limit} / 100), type => $event->{type} eq 'profile_limit' ? 'billing profile' : 'customer', }; foreach my $subscriber (@{$subscribers}) { $vars->{subscribers} .= sprintf "%s\@%s %s\n", @{$subscriber}{qw(username domain)}, $subscriber->{external_id} ? '(' . $subscriber->{external_id} . ')' : ''; } my $tt = Template->new(); foreach my $field (keys %{$template}) { my $out; $tt->process(\$template->{$field}, $vars, \$out); $template->{$field} = $out if $out; } die "'To' header is empty in the email\n" unless $event->{interval_notify}; die "'From' header is empty in the email\n" unless $template->{from_email}; die "'Subject' header is empty in the email\n" unless $template->{subject}; my $transport = Email::Sender::Transport::Sendmail->new; my $email = Email::Simple->create( header => [ To => $event->{interval_notify}, From => $template->{from_email}, Subject => $template->{subject}, ], body => $template->{body}, ); Email::Sender::Simple->send($email, { transport => $transport }) or die sprintf "Cannot send fraud daily lock notification to %s: $ERRNO\n", $email->header('To'); update_notify_status($event); return; } sub update_notify_status { my $event = shift; my $client = NGCP::API::Client->new(verbose => $opts->{verbose}); my $now = strftime('%Y-%m-%d %H:%M:%S', localtime); my $uri = '/api/customerfraudevents/' . $event->{id}; my $data = [ { op => 'replace', path => '/notify_status', value => 'notified', }, { op => 'replace', path => '/notified_at', value => $now, }, ]; my $res = $client->request('PATCH', $uri, $data); return; } sub main { if (my $num = $pf->running) { print "$PROGRAM_BASE is already running.\n"; $retcode = 1; exit $retcode; } mkdir $piddir unless -e $piddir; $pf->write; check_params(); load_config(); get_data( sprintf('/api/customerfraudevents/?no_count=true¬ify_status=%s', 'new'), 'customerfraudevents', sub { my $events = shift; foreach my $event (@{$events}) { if ($event->{interval_notify}) { my $subscribers = get_data(sprintf('/api/subscribers/?customer_id=%d', $event->{contract_id}), 'subscribers'); next unless scalar @$subscribers; eval { send_email($event, $subscribers); }; print $EVAL_ERROR if $EVAL_ERROR; } } } ); return; } sub cleanup { $pf->remove; rmdir $piddir if $piddir; } eval { main() }; if ($EVAL_ERROR) { print STDERR $EVAL_ERROR; $retcode = 1; } exit $retcode; __END__ =head1 NAME ngcp-fraud-notifier - send fraud notifications for customers exceeding thresholds =head1 SYNOPSIS B [I...] =head1 DESCRIPTION B checks for contract balances above fraud limit warning thresholds and sends email notifications about the incidents. =head1 OPTIONS =over 8 =item B<--verbose> Show additional debug information. Default false. =item B<--help> Print a brief help message. =back =head1 EXIT STATUS =over =item B<0> Command completed successfully. =item B<1> Command failed with errors. =back =head1 SEE ALSO NGCP::API::Client =head1 BUGS AND LIMITATIONS Please report problems you notice to the Sipwise Development Team . =head1 AUTHOR Kirill Solomko =head1 LICENSE Copyright (C) 2019 Sipwise GmbH, Austria 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, see . =cut