アスタリスクとActive Directoryの統合

組織の開発のある段階で、VoIPテレフォニーに切り替えることが決定されました。 プラットフォームとして無条件にアスタリスクPBXが選択されました。 端末機器は手頃な予算-DLink DPH-150から取得されました。

完了した作業の結果として、標準のMS ActiveDirectoryスナップインを介して制御される自動VoIPシステムが取得されました。



アスタリスク1.8.4は、Ubuntu 9.04のソースからコンパイルされました。

「アスタリスクアクティブディレクトリ」という検索フレーズを使用してインターネットを歩くと、アスタリスクの構成ファイルを生成するperlスクリプトに基づく部分統合を使用することが決定されました。 Asteriskコアに基づいたADとの完全な統合は、インターネット上でこれに関する情報が不足しているため、気のめいるように見えました。 選択した統合オプションを使用する理由は、クラウンによって起動され、構成ファイル「users.conf」を生成した単純なperlスクリプト(残念ながら見つかりませんでした)でした。その後、アスタリスクサービスが再起動されました。 アスタリスクの機能を徹底的に調査し、見つかったスクリプトの機能を拡張すると、次のことが判明しました。



users.confの構成と接続



以下のスクリプトは、「phone」属性が指定されているすべてのユーザーをADで検索し、users.conf構成形式でstdoutのユーザーのリストを表示します。

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 # ie 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 configを絶えず再フォーマットする必要があり、人的要因とともに不可避なエラーにつながります。 代替ソリューションの本質は、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 = '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 # ie 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 -rx reload
      
      





このような再起動では、サービス全体を再起動するのとは異なり、電話セッションは中断しません。



継続


記事がコミュニティの関心を引くものである場合、次のトピックを含めたい物語を続ける準備ができています。

  1. DLINK DPH-150電話および自動プロビジョニングをサポートするその他のデバイスの構成を自動的に展開します
  2. ADを介したNTLM認証による自動ダイヤルのためのDialFoxソフトウェアの使用。 特に、mod_ntlmをapache2にねじ込む




ご清聴ありがとうございました。



PSスクリプトを書く過程で、普遍性のためにすべてのコメントを英語で作成しようとしました。 しかし、残念ながら外国語の文法には、多くの要望が残されています。 コメントの主な意味が明確になることを願っています。



UPD:スクリプトを更新しました。 追加者:

1. DNSサーバーからのコントローラーのドメインの定義。

2.パラメータを使用してスクリプトを実行する機能-stdoutが書き込まれるファイルの名前。



All Articles