На некотором этапе развития нашей организации было решено перейти на VoIP телефонию. В качестве платформы безоговорочно был выбран Asterisk-PBX. Оконечное оборудование брали бюджетное из доступного — DLink DPH-150.
В результате проделанной работы получилась автоматизированная VoIP система, с управлением через стандартную оснастку MS ActiveDirectory.
Asterisk 1.8.4 собрали из исходников на Ubuntu 9.04.
Пройдясь по просторам интернета с поисковой фразой «asterisk active directory», было решено использоваться частичную интеграцию на базе скриптов perl, формирующих файлы конфигурации для asterisk. Полная интеграция с AD на базе ядра Asterisk выглядела удручающе в виду мизерного количества информации по этому поводу в интернете. Причиной использовать выбранный вариант интеграции стал один простенький скрипт на perl (к сожалению истоков которого уже и не найти), который запускался кроном и формировал конфигурационный файл 'users.conf', после чего служба asterisk перезапускалась. После тщательного исследования возможностей asterisk и расширения функционала найденного скрипта получилось следующее.
Нижеприведенный скрипт находит в AD всех пользователей, у которых указан атрибут 'phone' и выводит в stdout список пользователей в формате конфига users.conf.
В файл конфигурации users.conf, скрипт подключается следующим образом:
Сам скрипт users.pl:
Основной план нумерации расписан в ручную в файле конфигурации extensions.conf. Но в нашей организации довольно часто сотрудники переходят из одного отдела в другой, из за чего пришлось бы постоянно переформировывать конфиг extensions.conf, что в совокупности с человеческим фактором приводило бы к неминуемым ошибкам. Суть альтернативного решения такова, что в AD, в определенном OU (в скрипте $ADGroupsSearchBase) создаются группы, в 'description' которых пишется телефонный номер группы а в 'members' включаются те абоненты, на которые будет поступать звонок при наборе номера группы.
Скрипт в конфиге подключается так же:
Скрипт:
Вывод скрипта примерно такой:
Для автоматической подгрузки новых данных из AD в cron добавлено задание перезагрузки конфигурации asterisk:
При такой перезагрузке, в отличии от перезагрузки службы целиком, телефонные сеансы не обрываются.
Если статья вызовет интерес у сообщества, то готов продолжить повествование в которые хотел бы включить следующие темы:
Благодарю за уделенное внимание.
P.S. По ходу написания скриптов все комментарии старался составлять на английском языке для универсальности. Но к сожалению грамматика на ино-языке оставляет желать лучшего. Надеюсь основной смысл комментариев будет понятен.
UPD: Обновил скрипты. Добавил:
1. Определение домен контроллеров из DNS сервера.
2. Возможность запуска скрипта с параметром — именем файла, в который будет записываться stdout.
В результате проделанной работы получилась автоматизированная VoIP система, с управлением через стандартную оснастку MS ActiveDirectory.
Asterisk 1.8.4 собрали из исходников на Ubuntu 9.04.
Пройдясь по просторам интернета с поисковой фразой «asterisk active directory», было решено использоваться частичную интеграцию на базе скриптов perl, формирующих файлы конфигурации для asterisk. Полная интеграция с AD на базе ядра Asterisk выглядела удручающе в виду мизерного количества информации по этому поводу в интернете. Причиной использовать выбранный вариант интеграции стал один простенький скрипт на perl (к сожалению истоков которого уже и не найти), который запускался кроном и формировал конфигурационный файл 'users.conf', после чего служба asterisk перезапускалась. После тщательного исследования возможностей asterisk и расширения функционала найденного скрипта получилось следующее.
Формирование и подключение users.conf
Нижеприведенный скрипт находит в AD всех пользователей, у которых указан атрибут 'phone' и выводит в stdout список пользователей в формате конфига users.conf.
В файл конфигурации users.conf, скрипт подключается следующим образом:
#exec /etc/asterisk/scripts/users.pl
Сам скрипт 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("%03d",( 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) создаются группы, в 'description' которых пишется телефонный номер группы а в 'members' включаются те абоненты, на которые будет поступать звонок при наборе номера группы.
Скрипт в конфиге подключается так же:
#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 = 'asterisk@Organisation.com';
# 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, и других аппаратов, поддерживающих autoprovision
- Использование ПО DialFox для автоматического набора номеров с авторизацией NTLM через AD. В частности, прикручивание mod_ntlm к apache2
Благодарю за уделенное внимание.
P.S. По ходу написания скриптов все комментарии старался составлять на английском языке для универсальности. Но к сожалению грамматика на ино-языке оставляет желать лучшего. Надеюсь основной смысл комментариев будет понятен.
UPD: Обновил скрипты. Добавил:
1. Определение домен контроллеров из DNS сервера.
2. Возможность запуска скрипта с параметром — именем файла, в который будет записываться stdout.