artem
17.11.2016
11471

Проверка доступности городских номеров средствами Asterisk

Для владельца любой компании важно быть уверенным в том, что телефонные номера компании всегда доступны для звонков клиентов. К сожалению, полностью исключить отказ телефонии невозможно, но можно быстро узнать о проблеме и начать предпринимать меры по её устранению. В этой статье будет описан способ реализации периодической проверки городских номеров средствами Asterisk.

Для реализации данной схемы понадобится, как минимум, один дополнительный провайдер телефонии в добавок к тому провайдеру, доступность номеров которого мы будем проверять. В качестве примера предположим, что в Asterisk заведено два SIP-транка:
in_prov — провайдер используемый для входящей связи, доступность его номеров для входящих вызовов мы и будем проверять. Провайдер предоставляет номера 71111111111, 72222222222 и 73333333333.
out_prov — провайдер используемый для исходящей связи, он будет использоваться для совершения проверочных вызовов. Провайдер предоставляет номер 74444444444.
Проверочные вызовы будут идти с номера 74444444444 на номера 71111111111, 72222222222 и 73333333333
Скрипт для проверки доступности городских номеров /opt/trunk_checker/trunk_checker.pl будет запускаться раз в две минуты с помощью crond. После запуска скрипт поочерёдно совершает вызов на каждый из проверяемых номеров. После получения проверочного вызова, Asterisk записывает в AstDB информацию о получении вызова и отбивает вызов (т.к. Asterisk не принимает вызов, расходы на исходящую и, в случае номеров 8800, входящую связь будут минимизированы). Список неответивших номеров должен быть отправлен на адреса [email protected] и [email protected].

Диалплан
Нам понадобится контекст для обработки проверочных вызовов, код контекста:

[trunk-checker-dst]
exten => _X.,1,ResetCDR
same => n,NoCDR
same => n,Set(DB(trunk_checker/${EXTEN})=${EPOCH})
same => n(end),Busy(2)

Теперь добавим в контекст, куда приходят входящие вызовы, переход в контекст trunk-checker-dst, если вызов пришёл с номера 74444444444. В качестве примера будем считать, что входящие вызовы приходят в контекст from-trunk, но мы не будем вносить в него изменения, вместо этого создадим контекст from-trunk-pre, код контекста:

[from-trunk-pre]
exten => _X.,1,Set(DIALED_NUM=${EXTEN})
same => n,Goto(cont,1)
exten => _+X.,1,Set(DIALED_NUM=${EXTEN})
same => n,Goto(cont,1)
exten => cont,1,Gosub(sub-fix-cid,s,1)
same => n,GotoIf($[«${CALLERID(num)}» = «84444444444»]?trunk-checker-dst,${DIALED_NUM},1)
same => n,Goto(from-trunk,${DIALED_NUM},1)
[sub-fix-cid]
exten => s,1,GotoIf($[${REGEX(«^[0-9]{7}$» ${CALLERID(num)})} = 0]?cont1) ;»
same => n,Set(CALLERID(num)=8495${CALLERID(num)})
same => n,Set(CALLERID(ANI-num)=${CALLERID(num)})
same => n,Goto(end)
same => n(cont1),GotoIf(${REGEX(«^[78]?[2-9][0-9]{9}$» ${CALLERID(num)})}?fix_cid)
same => n,GotoIf(${REGEX(«^\+?7[2-9][0-9]{9}$» ${CALLERID(num)})}?fix_cid)
same => n,Goto(end)
same => n(fix_cid),Set(CALLERID(num)=8${CALLERID(num):-10})
same => n,Set(CALLERID(ANI-num)=${CALLERID(num)})
same => n(end),Return

Контекст from-trunk-pre также вызывает контекст sub-fix-cid для приведения номера звонящего к общему виду (11 цифр с восьмёрки), что является хорошей идеей на случай, если нужно будет проверять доступность номеров, предоставляемых разными провайдерами (т.к. разные провайдеры могут передавать номер звонящего в разном виде). Далее все вызовы не с номера 74444444444 будут попадать в контекст from-trunk.
Теперь нужно завернуть входящие вызовы через проверяемый транк в контекст from-trunk-pre (указать для транка in_prov параметр context=from-trunk-pre). Обязательно убедитесь, что входящие вызовы через транк in_prov работают нормально перед продолжением настройки.
Контекст для совершения проверочных вызовов:

[trunk-checker-dial]
exten => _X.,1,GotoIf($[«${CHECK_THRU_TRUNK}» = «»]?end)
same => n,Dial(SIP/${CHECK_THRU_TRUNK}/${EXTEN},60)
same => n(end),Hangup

Нужен ещё один контекст для приёма второго плеча, создаваемого AMI-действием Originate. Всё, что будет делать этот контекст — принимать вызов ожидать две секунды и разрывать вызов, код контекста:

[trunk-checker-src]
exten => s,1,ResetCDR
same => n,NoCDR
same => n,Answer
same => n,Wait(2)
same => n,Hangup

На самом деле, при текущем алгоритме проверки доступности номеров, контекст trunk-checker-src не должен вызываться никогда, т.к. Asterisk будет всегда отбивать проверочный вызов, но контекст лучше всё равно создать на тот случай, если Asterisk всё-таки ответит на проверочный вызов, например, из-за ошибки в настройках.

Скрипт проверки городских номеров
Код скрипта /opt/trunk_checker/trunk_checker.pl приведён ниже:

#!/usr/bin/perl
use warnings;
use strict;
use IO::Socket;
#use Data::Dumper;
use utf8;
use constant AMI_CONNECT_TIMEOUT => 3;
use constant AMI_READ_TIMEOUT => 30;
use constant AMI_RES_VALUE => 0;
use constant AMI_RES_ACTIONID => 1;
use constant AMI_RES_DATA => 2;
# PID
use constant PID_DIR => ‘/var/run/trunk_checker’;
use constant PID_FILE => ‘trunk_checker’;
# Get script name and path
my $cwd = »;
my $script_name = »;
if($0 =~ /^(.*)/([^/]+)$/) { $cwd = $1; $script_name = $2 } else { print «Couldn’t determine cwd (WTF?)»; exit 1 }
# Variables
my $config_file = $cwd.’/trunk_checker.conf’;
our @check_nums = ();
my @failed_check = ();
our %config = ();
my $check_delay = 6;
my $allowed_time_diff = 40;
my $sendEmail = $cwd.’/sendEmail.pl’;
unless(do $config_file) {
print «ERROR Config file $config_file is missing or invalid: $!n»;
exit 1;
}
if(not defined $config{trunk} or $config{trunk} eq » or not defined $config{context} or $config{context} eq » or not defined $config{cid} or not defined $config{db_family} or $config{db_family} eq ») {
print «ERROR Config file $config_file is missing required parameters (trunk, context, cid, db_family are required)n»;
exit 1;
}
if(not defined $config{ami_host} or $config{ami_host} eq » or not defined $config{ami_port} or $config{ami_port} eq » or not defined $config{ami_user} or $config{ami_user} eq » or not defined $config{ami_secret} or $config{ami_secret} eq ») {
print «ERROR Config file $config_file is missing AMI connection parameters (ami_host, ami_port, ami_user, ami_secret)n»;
exit 1;
}
### PID stuff
my $pid_file = PID_DIR.’/’.PID_FILE;
# Check if still running
my $cur_pid = »;
if(-r $pid_file) {
if(open PID, $pid_file) {
$cur_pid = <PID>;
chomp $cur_pid;
$cur_pid = » unless $cur_pid =~ /^[0-9]+$/;
close PID;
}
}
if($cur_pid) {
if(open PS, «ps -p $cur_pid -o comm,args —no-headers |») {
while(<PS>) {
chomp $_;
if($_ =~ /$script_name/) { print «Still running $cur_pidn»; exit }
}
close PS;
}
}
# Leave PID file
unless(-d PID_DIR) { mkdir PID_DIR or print «Couldn’t create «.PID_DIR.»n» }
if(open PID, «>$pid_file») {
PID->autoflush(1);
print PID «$$n»;
close PID;
} else { print «Couldn’t write PID file $pid_filen» }
###
# connect to Asterisk AMI
my $ami_sock = ami_connect($config{ami_host},$config{ami_port},$config{ami_user},$config{ami_secret});
if(! $ami_sock) { print «ERROR Couldn’t connect to «.$config{ami_host}.»:».$config{ami_port}.»n»; exit 1 }
my $num_i = 0;
foreach my $num (@check_nums) {
$num_i++;
my $chan = ‘Local/’.$num.’@trunk-checker-dial/n’;
my $exten = ‘s’;
my $prio = ‘1’;
ami_orig_app($ami_sock,$chan,$config{context},$exten,$prio,$config{cid},»,{ CHECK_THRU_TRUNK => $config{trunk} });
sleep($check_delay);
my $db_key = $num;
my $resp = ami_dbget($ami_sock,$config{db_family},$db_key);
if(defined $resp->[AMI_RES_DATA] and @{$resp->[AMI_RES_DATA]}) {
my $call_receive_time = $resp->[AMI_RES_DATA]->[0];
if($call_receive_time =~ /^d+$/) {
my $now = time();
if(abs($call_receive_time — $now) > $allowed_time_diff) {
push(@failed_check,$num);
}
} else { push(@failed_check,$num); }
} else { push(@failed_check,$num); }
sleep(3);
}
if(@failed_check) {
my $subj = ‘Часть номеров не ответила при проверке доступности номеров’;
my $message = «Следующие номера не ответили на тестовый вызов:n».join(‘, ‘,@failed_check);
if(defined $config{mail_to} and $config{mail_to} ne ») {
‘$sendEmail -u ‘$subj’ -t ‘$config{mail_to}’ -m ‘$message’ -o message-charset=UTF-8′;
}
}
close $ami_sock;
###
### SCRIPT END
###
### Connect to Asterisk manager interface
### login and password are read from /etc/asterisk/manager.conf
sub ami_connect {
my ($ami_host, $ami_port, $ami_user, $ami_pass) = @_;
my $sock = 0;
$sock = IO::Socket::INET->new(
Proto => «tcp»,
PeerAddr => $ami_host,
PeerPort => $ami_port,
Timeout => AMI_CONNECT_TIMEOUT
);
$sock or return 0;
$sock->autoflush(1);
my $action_id = gen_pass(10);
my $login_msg = «Action: loginrnActionID: $action_idrnUsername: $ami_userrnSecret: $ami_passrnEvents: offrnrn»;
print $sock «$login_msg»;
my $response = [«», «»];
while($response->[AMI_RES_ACTIONID] ne $action_id) {
$response = read_ami_message($sock);
if($response->[AMI_RES_VALUE] eq «Error») {
print «ERROR Authentication failedn»;
close $sock;
return 0;
} elsif($response->[AMI_RES_VALUE] eq «») {
print «ERROR AMI read timed outn»;
close $sock;
return 0;
}
}
return $sock;
}
sub ami_orig_app {
my ($sock, $chan, $cont, $ext, $prio, $cid, $src_ext, $vars) = @_;
if($cid =~ /[^0-9+]/) {
if($src_ext) { $cid = $cid.'<‘.$src_ext.’>’ } else { $cid = $cid.'<‘.$ext.’>’ }
}
my $action_id = gen_pass(10);
my $msg = «Action: OriginaternActionID: $action_idrnChannel: $chanrnContext: $contrnExten: $extrnPriority: $priornTimeout: 14000rnCallerid: $cidrn»;
my $var_str=»»;
foreach (keys %$vars) {
$var_str = $var_str.»$_=$vars->{$_}|»;
}
$msg = $msg.»Variable: «.substr($var_str,0,-1).»rn» if $var_str ne «»;
$msg = $msg.»rn»;
print $sock «$msg»;
my $ami_message = read_ami_message($sock);
if($ami_message->[AMI_RES_VALUE] eq «») { print «ERROR AMI read timed outn» }
return $ami_message;
}
sub ami_dbget {
my $sock = ${shift(@_)};
my ($family, $key) = @_;
my $action_id = gen_pass(10);
my $msg = «Action: DBGetrnActionID: $action_idrnFamily: $familyrnKey: $keyrnrn»;
print $sock «$msg»;
my $response = [», »];
my $res = [‘dummy’, »];
while($res->[AMI_RES_ACTIONID] ne $action_id) {
$res = read_ami_message($sock,’DBGet’,AMI_READ_TIMEOUT);
if($res->[AMI_RES_VALUE] eq «») {
print STDERR «ERROR AMI DBGet read timed outn$msg»;
return $response;
} elsif($res->[AMI_RES_VALUE] eq «Success») {
$response->[AMI_RES_DATA] = [];
my $res1 = [‘dummy’, »];
while($res1->[AMI_RES_VALUE] ne ») {
$res1 = read_ami_message($sock,’DBGetResponse’,2);
if($res1->[AMI_RES_ACTIONID] eq $action_id) {
if($res1->[AMI_RES_VALUE] eq ‘DBGetResponse’) {
if(defined $res1->[AMI_RES_DATA]->{‘Val’}) {
push(@{$response->[AMI_RES_DATA]},$res1->[AMI_RES_DATA]->{‘Val’});
} else {
print STDERR «ERROR ‘Val’ not defined in DBGetResponsen»;
}
} elsif($res1->[AMI_RES_VALUE] eq ‘DBGetComplete’) {
last;
} elsif($res1->[AMI_RES_VALUE] eq ») {
print STDERR «ERROR AMI DBGetResponse read timed outn»;
last;
} else {
print STDERR «ERROR AMI DBGetResponse read returned unexpected response $res1->[AMI_RES_VALUE]n»;
last;
}
}
}
}
}
return $response;
}
#
# Parse AMI message, return @result
# $result[0] = Event or Response
# $result[1] = ActionID
# $result[2] = hash of parameters
#
sub read_ami_message {
my ($sock,$log_label,$timeout) = shift(@_);
if(not defined $log_label) { $log_label = ‘Unspecified’; }
if(not defined $timeout) { $timeout = AMI_READ_TIMEOUT; }
my @result = («», «», {});
eval {
local $SIG{ALRM} = sub { exit };
alarm $timeout;
my $ami_line = «start»;
while($ami_line ne «») {
$ami_line = <$sock>;
if(not defined $ami_line) { last; }
$ami_line =~ s/rn//g;
#print «$ami_linen»;
if($ami_line =~ /^([^:]+): (.*)$/) {
if($1 eq «Event» or $1 eq «Response») { $result[AMI_RES_VALUE] = $2 }
elsif($1 eq «ActionID») { $result[AMI_RES_ACTIONID] = $2 }
else { $result[AMI_RES_DATA]->{$1} = $2 }
}
}
};
return @result;
}
### Generate password
### gen_pass(pass_len,var,num_only)
### var — if set, pass_len will randomly vary by +-2 characters
### num_len — if set, only numbers will be used in password generation
sub gen_pass {
my $pass_len = shift(@_);
my $var = shift(@_);
my $num_only = shift(@_);
my @chars = (‘a’..’z’,’A’..’Z’,’0′..’9′,’_’,’-‘);
if($var) { $pass_len = int(rand 4) + $pass_len — 2 }
if($num_only) { @chars = (‘0’..’9′) }
my $pass = «»;
foreach (1..$pass_len) { $pass .= $chars[rand @chars]; }
return $pass;
}

В одной директории со скриптом должен находиться файл настроек trunk_checker.conf, пример файла настроек:

%config = (
mail_to => ‘[email protected],[email protected]’,
trunk => ‘out_prov’,
context => ‘trunk-checker-src’,
cid => ‘74444444444’,
db_family => ‘trunk_checker’,
ami_host => ‘localhost’,
ami_port => ‘5038’,
ami_user => ‘admin’,
ami_secret => ‘admin_secret’,
);
@check_nums = (
‘81111111111’,
‘82222222222’,
‘83333333333’,
);

Описание параметров скрипта:
mail_to — список адресов электронной почты, на которые нужно отправлять уведомление о неответивших номерах (разделитель — запятая)
trunk — имя SIP-транка, через который будут совершаться проверочные вызовы
context — имя контекста для второго плеча AMI-действия Originate (в контексте должен быть определён экстеншен s)
cid — номер, который нужно подставлять при исходящих вызовах через out_prov
db_family — имя ветки в AstDB, в которой скрипт будет проверять время получения последнего проверочного вызова (в диалплане Asterisk должна быть указана та же ветка для сохранения время получения последнего проверочного вызова)
ami_host — хост для подключения по AMI
ami_port — порт для подключения по AMI
ami_user — имя пользователя для подключения по AMI
ami_secret — пароль для подключения по AMI
Для отправки уведомлений по почте используется скрипт sendEmail.pl, он должен находиться в одной директории со скриптом trunk_checker.pl. Скрипт sendEmail.pl можно скачать здесь http://caspian.dotconf.net/menu/Software/SendEmail/sendEmail-v1.56.tar.gz. Настройки SMTP-сервера для отправки почты нужно указать в теле скрипта sendEmail.pl (параметры server, port, username, password и from).
Для тестирования запускайте скрипт вручную, следите за выводом скрипта и за выводом в консоли Asterisk, После того, как скрипт заработает нормально, добавьте его в crontab, с помощью похожей строки:

*/2 * * * * /opt/trunk_checker/trunk_checker.pl

 

Один из наших читателей собрал рабочий скрипт, если кто-то захочет повторить: http://vxlk.ru/numcheck
Книга 101 функция Asterisk
Познакомьтесь с возможностями Asterisk. Найдите инструменты, которые помогут вашей компании развиваться.
Скачать книгу
Подписаться
Уведомить о
guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Остались вопросы?

Я - Виталий Шелест, менеджер компании Voxlink. Хотите уточнить детали или готовы оставить заявку? Укажите номер телефона, я перезвоню в течение 3-х секунд.

VoIP оборудование


ближайшие курсы

10 доводов в пользу Asterisk

Распространяется бесплатно.

Asterisk – программное обеспечение с открытым исходным кодом, распространяется по лицензии GPL. Следовательно, установив один раз Asterisk вам не придется дополнительно платить за новых абонентов, подключение новых транков, расширение функционала и прочие лицензии. Это приближает стоимость владения станцией к нулю.

Безопасен в использовании.

Любое программное обеспечение может стать объектом интереса злоумышленников, в том числе телефонная станция. Однако, сам Asterisk, а также операционная система, на которой он работает, дают множество инструментов защиты от любых атак. При грамотной настройке безопасности у злоумышленников нет никаких шансов попасть на станцию.

Надежен в эксплуатации.

Время работы серверов некоторых наших клиентов исчисляется годами. Это значит, что Asterisk работает несколько лет, ему не требуются никакие перезагрузки или принудительные отключения. А еще это говорит о том, что в районе отличная ситуация с электроэнергией, но это уже не заслуга Asterisk.

Гибкий в настройке.

Зачастую возможности Asterisk ограничивает только фантазия пользователя. Ни один конструктор шаблонов не сравнится с Asterisk по гибкости настройки. Это позволяет решать с помощью Asterisk любые бизнес задачи, даже те, в которых выбор в его пользу не кажется изначально очевидным.

Имеет огромный функционал.

Во многом именно Asterisk показал какой должна быть современная телефонная станция. За многие годы развития функциональность Asterisk расширилась, а все основные возможности по-прежнему доступны бесплатно сразу после установки.

Интегрируется с любыми системами.

То, что Asterisk не умеет сам, он позволяет реализовать за счет интеграции. Это могут быть интеграции с коммерческими телефонными станциями, CRM, ERP системами, биллингом, сервисами колл-трекинга, колл-бэка и модулями статистики и аналитики.

Позволяет телефонизировать офис за считанные часы.

В нашей практике были проекты, реализованные за один рабочий день. Это значит, что утром к нам обращался клиент, а уже через несколько часов он пользовался новой IP-АТС. Безусловно, такая скорость редкость, ведь АТС – инструмент зарабатывания денег для многих компаний и спешка во внедрении не уместна. Но в случае острой необходимости Asterisk готов к быстрому старту.

Отличная масштабируемость.

Очень утомительно постоянно возвращаться к одному и тому же вопросу. Такое часто бывает в случае некачественного исполнения работ или выбора заведомо неподходящего бизнес-решения. С Asterisk точно не будет такой проблемы! Телефонная станция, построенная на Asterisk может быть масштабируема до немыслимых размеров. Главное – правильно подобрать оборудование.

Повышает управляемость бизнеса.

Asterisk дает не просто набор полезных функций, он повышает управляемость организации, качества и комфортности управления, а также увеличивает прозрачность бизнеса для руководства. Достичь этого можно, например, за счет автоматизации отчетов, подключения бота в Telegram, санкционированного доступа к станции из любой точки мира.

Снижает расходы на связь.

Связь между внутренними абонентами IP-АТС бесплатна всегда, независимо от их географического расположения. Также к Asterisk можно подключить любых операторов телефонии, в том числе GSM сим-карты и настроить маршрутизацию вызовов по наиболее выгодному тарифу. Всё это позволяет экономить с первых минут пользования станцией.