Интеграция Asterisk С Active Directory

На каком-то этапе развития нашей организации было принято решение перейти на 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 следующим образом:
  
  
  
  
   

#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("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

При такой перезагрузке, в отличие от перезагрузки всей службы, телефонные сеансы не прерываются.



Продолжение
Если статья вызовет интерес у сообщества, я готов продолжить рассказ, в который хотелось бы включить следующие темы:
  1. Автоматическое развертывание конфигурации телефонов DLINK DPH-150 и других устройств, поддерживающих автоинициализацию.

  2. Использование программного обеспечения DialFox для автоматического набора номеров с авторизацией NTLM через AD. В частности прикручивая mod_ntlm к apache2
Спасибо за внимание.

P.S. При написании скриптов я старался все комментарии писать на английском языке для универсальности.

Но, к сожалению, грамматика на иностранном языке оставляет желать лучшего.

Надеюсь, основной смысл комментариев будет понятен.

УПД: Обновлены скрипты.

Добавлен: 1. Определение контроллеров домена по DNS-серверу.

2. Возможность запуска скрипта с параметром — именем файла, в который будет записан stdout. Теги: #asterisk #active каталог #Active Directory #Разработка систем связи

Вместе с данным постом часто просматривают: