На каком-то этапе развития нашей организации было принято решение перейти на VoIP-телефонию.
В качестве платформы была безоговорочно выбрана Asterisk-PBX. Терминальное оборудование было взято от бюджетного, доступного — DLink DPH-150. В результате проделанной работы создана автоматизированная система VoIP, управляемая через стандартное оборудование MS ActiveDirectory. Asterisk 1.8.4 был скомпилирован из исходного кода в Ubuntu 9.04. После просмотра Интернета по поисковой фразе «активный каталог asterisk» было решено использовать частичную интеграцию на основе perl-скриптов, генерирующих файлы конфигурации для asterisk. Полная интеграция с AD на базе ядра Asterisk выглядела удручающе, учитывая скудное количество информации по этому поводу в Интернете.
Причиной использования выбранного варианта интеграции стал один простой perl-скрипт (к сожалению, исходники которого уже не удалось найти), который был запущен cron и сгенерировал конфигурационный файл 'users.conf', после чего сервис asterisk был перезапущен.
.
После тщательного изучения возможностей астериска и расширения функционала найденного скрипта выяснилось следующее.
Формирование и подключение user.conf
Приведенный ниже сценарий находит всех пользователей в AD, у которых указан атрибут «телефон», и выводит список пользователей на стандартный вывод в формате конфигурации user.conf. Скрипт подключается к файлу конфигурации user.conf следующим образом:Сам скриптusers.pl:#exec /etc/asterisk/scripts/users.pl
#!/usr/bin/perl
# users.pl v1.1
#
# Script to generate asterisk 'users.conf' file from Active Directory (LADP) on users which contains 'phone' attribute
#
# Using:
# 1. Print users to STDOUT:
# users.pl
#
# 2. Print users to file:
# users.pl users_custom.conf
use strict;
use warnings;
use Net::LDAP;
use Lingua::Translit;
######################
### BEGIN SETTINGS ###
######################
my $debug = 0;
my $warning = 0;
# name of Domain
my $AD="domain";
# Domain name in format AD
# for example mydomain.ru
my $ADDC="DC=domain";
# user in Active directory
# example: "CN=asterisk,CN=Users,$ADDC"
my $ADUserBind="CN=asterisk,CN=Users,$ADDC";
my $ADpass="p@s$w0rd";
# base search tree
# example "OU=Users,$ADDC"
my $ADUsersSearchBase="OU=Organisation,$ADDC";
# Field in active directory where telephone number, display name, phone stored
# "telephonenumber", "displayname", "mail"
my $ADfieldTelephone="telephonenumber";
my $ADfieldFullName="displayname";
my $ADfieldMail="mail";
my $ADfieldUser="samaccountname";
# You need to create a dialplan in your asterisk server;
my $dialplan="office";
# default settings
my $user_static =
"context = $dialplan
call-limit = 100
type = friend
registersip = no
host = dynamic
callgroup = 1
threewaycalling = no
hasdirectory = no
callwaiting = no
hasmanager = no
hasagent = no
hassip = yes
hasiax = yes
nat=yes
qualify=yes
dtmfmode = rfc2833
insecure = no
pickupgroup = 1
autoprov = no
label =
macaddress =
linenumber = 1
LINEKEYS = 1
callcounter = yes
disallow = all
allow = ulaw,alaw,iLBC,h263,h263p
";
#######################
### END OF SETTINGS ###
#######################
my $ldap;
# get array DNS names of AD controllers
my $dig = "dig -t srv _ldap._tcp.$AD" .
'| grep -v "^;\|^$" | grep SRV | awk "{print \$8}"';
my @adControllers = `$dig`;
# try connect to AD controllers
foreach my $controller (@adControllers){
$controller =~ s/\n//;
#INITIALIZING
$ldap = Net::LDAP->new ( $controller ) or next;
print STDERR "Connected to AD controller: $controller\n" if $debug > 0;
last;
}
die "$@" unless $ldap;
my $mesg = $ldap->bind ( dn=>$ADUserBind, password =>$ADpass);
#PROCESSING - Displaying SEARCH Results
# Accessing the data as if in a structure
# i.e. Using the "as_struct" method
my $ldapUsers = LDAPsearch (
$ADUsersSearchBase,
"$ADfieldTelephone=*",
[ $ADfieldFullName, $ADfieldTelephone, $ADfieldMail, $ADfieldUser ]
)->as_struct;
# translit RUS module.
# GOST 7.79 RUS, reversible, GOST 7.79:2000 (table B), Cyrillic to Latin, Russian
my $tr = new Lingua::Translit("GOST 7.79 RUS");
my %hashPhones = ();
my $phones = \%hashPhones;
my @out;
while ( my ($distinguishedName, $attrs) = each(%$ldapUsers) ) {
# if not exist phone or name - skipping
my $attrPhone = $attrs->{ "$ADfieldTelephone" } || next;
my $attrUser = $attrs->{ "$ADfieldUser" } || next;
my $attrName = $attrs->{ "$ADfieldFullName" } || next;
my $encName = $tr->translit("@$attrName");
my $attrMail = $attrs->{ "$ADfieldMail" } || [""];
# check for duplicates phone number
if ( $phones -> {"@$attrPhone"} ){
my $currUser = "@$attrName";
my $existUser = $phones -> {"@$attrPhone"};
print STDERR "@$attrPhone alredy exist! Exist:'$existUser' Current:'$currUser'.
skipping - '[@$attrPhone] $currUser'\n" if $warning;
next;
} else {
$phones -> {"@$attrPhone"} = "@$attrName";
}
# password for SID = (telephonenumber without first digit) + 1
# example: phone=6232 pass=233
#$phsecret =sprintf("d",( substr("@$attrVal",1,100)+1));
my $phsecret = "@$attrPhone";
push (@out,
"[@$attrPhone]\n"
.
"fullname = $encName\n"
.
"email = @$attrMail\n"
.
"username = @$attrUser\n"
#.
"mailbox = @$attrPhone\n"
.
"cid_number = @$attrPhone\n"
.
"vmsecret = $phsecret\n"
.
"secret = $phsecret\n"
.
"transfer = yes\n"
.
"$user_static\n"
);
} # End of that DN
# print to file
if (@ARGV){
open FILE, "> $ARGV[0]" or die "Error create file '$ARGV[0]': $!";
print STDOUT "Printing to file '$ARGV[0]'";
print FILE @out;
close FILE;
print STDOUT " .
done!\n";
}
# print to STDOUT
else{
print @out;
}
exit 0;
#OPERATION - Generating a SEARCH
#$base, $searchString, $attrsArray
sub LDAPsearch
{
my ($base, $searchString, $attrs) = @_;
my $ret = $ldap->search ( base => $base,
scope => "sub",
filter => $searchString,
attrs => $attrs
);
LDAPerror("LDAPsearch", $ret) && die if( $ret->code );
return $ret;
}
sub LDAPerror
{
my ($from, $mesg) = @_;
my $err = "[$from] - error"
.
"\nCode: " .
$mesg->code
.
"\nError: " .
$mesg->error .
" (" .
$mesg->error_name .
")"
.
"\nDescripton: " .
$mesg->error_desc .
".
" .
$mesg->error_text;
print STDERR $err if $warning;
}
Формирование телефонных групп на основе групп AD
Базовый план нумерации указывается вручную в файле конфигурации Extensions.conf. Но в нашей организации довольно часто сотрудники переходят из одного отдела в другой, из-за чего нам приходилось постоянно реформировать конфиг Extensions.conf, что вкупе с человеческим фактором приводило бы к неизбежным ошибкам.Суть альтернативного решения заключается в том, что в AD в определенном OU (в скрипте $ADGroupsSearchBase) создаются группы, в "описании" которых пишется телефонный номер группы, а в "участниках" включаются тех абонентов, которым будет подан звонок при наборе группового номера.
Скрипт в конфиге подключается аналогично: #exec /etc/asterisk/scripts/exten.pl
Скрипт: #!/usr/bin/perl
# exten.pl v1.1
#
# Script to generate extensions 'extensions_custom.conf' file,
# from Active Directory (LADP) on groups in OU=ADGroupsSearchBase
# which groups contains 'description' attribute
#
# Using:
# 1. Print users to STDOUT:
# exten.pl
#
# 2. Print users to file:
# exten.pl exten_custom.conf
use strict;
use warnings;
use Net::LDAP;
use Lingua::Translit;
######################
### BEGIN SETTINGS ###
######################
my $debug = 0;
my $warning = 1;
#name of Domain
my $AD="domain";
#Domain name in format AD
#for example mydomain.ru
my $ADDC="DC=domain";
# user in Active directory
# example: "CN=asterisk,CN=Users,$ADDC"
my $ADUserBind="CN=asterisk,CN=Users,$ADDC";
my $ADpass="p@s$w0rd";
# base search Groups tree example "OU=Users,$ADDC"
my $ADGroupsSearchBase = "OU=asterisk,OU=Groups,OU=Organisation,$ADDC";
# base search Users tree example "OU=Users,$ADDC"
my $ADUsersSearchBase = "OU=Organisation,$ADDC";
# default email to send voicemail if email user not set
my $defaultEmail = '[email protected]';
# Field in active directory where telephone number, display name, phone stored .
# "telephonenumber", "displayname", "mail", .
my $ADfieldTelephone = "telephonenumber"; my $ADfieldMember = "member"; my $ADfieldMemberOf = "memberof"; my $ADfieldInfo = "info"; my $ADfieldDescription = "description"; my $ADfieldMail = "mail"; ####################### ### END OF SETTINGS ### ####################### my $ldap; # get array DNS names of AD controllers my @adControllers = `dig -t srv _ldap._tcp.$AD | grep -v '^;\\|^\$' | grep SRV | awk '{print \$8}'`; # try connect to AD controllers foreach my $controller (@adControllers){ $controller =~ s/\n//; #INITIALIZING $ldap = Net::LDAP->new ( $controller ) or next; print STDERR "Connected to AD controller: $controller\n" if $debug > 0; last; } die "$@" unless $ldap; my $mesg = $ldap->bind ( dn=>$ADUserBind, password =>$ADpass); #PROCESSING - Displaying SEARCH Results # Accessing the data as if in a structure # i.e. Using the "as_struct" method my $ldapGroups = LDAPsearch ( $ADGroupsSearchBase, "$ADfieldDescription=*", [ $ADfieldMember, $ADfieldDescription ] )->as_struct; # translit RUS module. # GOST 7.79 RUS, reversible, GOST 7.79:2000 (table B), Cyrillic to Latin, Russian my $tr = new Lingua::Translit("GOST 7.79 RUS"); my $hash = (); # process each group in $ADGroupsSearchBase with phone while ( my ($distinguishedName, $groupAttrs) = each(%$ldapGroups) ) { print STDERR "Processing GROUP: [$distinguishedName]\n" if $debug > 1; my $attrMembers = $groupAttrs->{ $ADfieldMember } or next; my $desc = $groupAttrs->{ $ADfieldDescription } or next; my $groupNumber = "@$desc"; print STDERR "MEMBERS: @$attrMembers\nDESC: $groupNumber (Count=$#$attrMembers+1)" if $debug > 1; # process members in current group foreach my $member (@$attrMembers) { my $ldapMember = LDAPsearch( $ADUsersSearchBase, "$ADfieldTelephone=*", [ $ADfieldTelephone ] ) -> as_struct; my $memberAttrs = $ldapMember->{$member}; my $memberPhone = $memberAttrs->{$ADfieldTelephone}[0] or next; print STDERR "\nMEMBER: $member" if $debug > 1; print STDERR "\tPHONE:$memberPhone" if $debug > 1; if ($hash -> {$groupNumber}){ my $a = $hash -> {$groupNumber}; push @$a, $memberPhone; } else { $hash -> {$groupNumber} = [$memberPhone]; } } print STDERR "\n\n" if $debug > 1; } # End of that groups in $ADGroupsSearchBase my @out; while ( my ($groupPhone, $userPhones) = each (%$hash) ) { print STDERR "GROUP: $groupPhone\t PHONES: @$userPhones\n" if $debug > 1; #foreach my $userPhone (@$userPhones) { push (@out, "exten => $groupPhone,1,Dial(sip/" .
join('&sip/', @$userPhones) .
")\n"); } # print to file if (@ARGV){ open FILE, "> $ARGV[0]" or die "Error create file '$ARGV[0]': $!"; print STDOUT "Printing to file '$ARGV[0]'"; print FILE @out; close FILE; print STDOUT " .
done!\n"; } # print to STDOUT else{ print @out; } exit 0; #OPERATION - Generating a SEARCH # $base, $searchString, $attrsArray sub LDAPsearch { my ($base, $searchString, $attrs) = @_; my $ret = $ldap->search ( base => $base, scope => "sub", filter => $searchString, attrs => $attrs ); LDAPerror("LDAPsearch", $ret) && die if( $ret->code ); return $ret; } sub LDAPerror { my ($from, $mesg) = @_; my $err = "[$from] - error" .
"\nCode: " .
$mesg->code .
"\nError: " .
$mesg->error .
" (" .
$mesg->error_name .
")" .
"\nDescripton: " .
$mesg->error_desc .
".
" .
$mesg->error_text; print STDERR $err if $warning; #print STDERR "\nServer error: " .
$mesg->server_error if $debug;
}
Вывод скрипта примерно такой: exten => 605,1,Dial(sip/157&sip/130&sip/444&sip/103&sip/119&sip/151&sip/117)
exten => 602,1,Dial(sip/122&sip/110&sip/106)
exten => 607,1,Dial(sip/444&sip/122&sip/110&sip/100&sip/101)
exten => 601,1,Dial(sip/155&sip/101)
exten => 606,1,Dial(sip/444&sip/110&sip/100&sip/101)
Автоматизация
Для автоматической загрузки новых данных из AD в cron добавлена задача по перезагрузке конфигурации asterisk: asterisk -rx reload
При такой перезагрузке, в отличие от перезагрузки всей службы, телефонные сеансы не прерываются.
Продолжение
Если статья вызовет интерес у сообщества, я готов продолжить рассказ, в который хотелось бы включить следующие темы:- Автоматическое развертывание конфигурации телефонов DLINK DPH-150 и других устройств, поддерживающих автоинициализацию.
- Использование программного обеспечения DialFox для автоматического набора номеров с авторизацией NTLM через AD. В частности прикручивая mod_ntlm к apache2
P.S. При написании скриптов я старался все комментарии писать на английском языке для универсальности.
Но, к сожалению, грамматика на иностранном языке оставляет желать лучшего.
Надеюсь, основной смысл комментариев будет понятен.
УПД: Обновлены скрипты.
Добавлен: 1. Определение контроллеров домена по DNS-серверу.
2. Возможность запуска скрипта с параметром — именем файла, в который будет записан stdout. Теги: #asterisk #active каталог #Active Directory #Разработка систем связи
-
Онлайн-Хранилище Данных
19 Oct, 24 -
Кордова. Опыт Корпоративных Проектов
19 Oct, 24 -
Отладчик Прошел Через: Debuggerstepthrough.
19 Oct, 24