2012/04/05

Asterisk sync with Active Directory

In my case, all users information is stored in AD (yes, we use Exchange GAL). It'll be great if asterisk get this information directly from AD without need in configuration editing.

Let's look what we have now:
AD user have fields 'telephoneNumber' - for internal extension number, 'fax' - for direct inward dialing number (DID) like this:


From the asterisk side:
users.conf
[lan](!)
type=peer
host=dynamic
...

;And all users as child from this template

[502](lan)
fullname = Ryabov; Needed for username display on snom phones
secret = *** ;Your super secret password 
email = [email protected]; I use for fax to email
callgroup = 1; Number of room for call pickup
pickupgroup = 1
context = 1234567; DID for external calls
sip.conf
All my DIDs with same context name
[1234567]
type=friend
host=8.8.8.8
fromuser = 1234567
secret = ***
context=from-sats
...
extensions.conf
Outgoing extensions, same as DIDs
[1234567]
include => dial_internal
exten => _XXX.,1,Dial(SIP/1234567/${EXTEN},60);
same => n,Hangup;

;Incoming calls
[from-sats]
exten => 1234567,1,SIPAddHeader("Alert-Info:<http://nohost>\;info=alert-external\;x-line-id=0"); Different ring melody for external calls
same => n,Dial(SIP/502&SIP/503,60,r); Internal number to dial
same => n,Hangup;

So what is possible to automate here? In my case we have different correspondence between DID and internal number for incoming calls. But always have one DID for external calls, which is stored in AD in user info. My idea in brief - dynamically generate users.conf by information provided in AD. No need any changes to sip.conf and extensions.conf outgoing calls section. For incoming calls section - it's impossible to automate, cuz all departments wants custom incoming dialplan for their's DID.
Ok, let's do it!) All we need is PHP installed with
extension=php_ldap.so
in php.ini enabled.
Save this script as /etc/asterisk/ldap.php and cmod +x it
#!/usr/bin/php
<?
//by sepa.spb.ru v23.09.2011
//config------------------------------------------------------------------------
$cache='/etc/asterisk/users.cache'; //cache used when no connection to DC
$ldap['srv']='dc.crpp.ru';          //LDAP server
$ldap['user']='[email protected]';        //user to bind with  
$ldap['pass']='***';                //user password
$ldap['dn']='DC=crpp,DC=ru';        //DN path to filter users
$conf['tpl']='lan';                 //template name in users.conf
$conf['mail']='[email protected]';     //error notify
 //------------------------------------------------------------------------------ 
$error='';
$out='';

$ldap['dc'] = ldap_connect($ldap['srv']) or die("Could not connect to LDAP!</br>");
ldap_set_option($ldap['dc'],LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap['dc'],LDAP_OPT_REFERRALS, 0);
if(ldap_bind($ldap['dc'], $ldap['user'], $ldap['pass'])) {
  $filter = "(&(telephoneNumber=*)(objectCategory=user)(objectClass=user)(samAccountName=*))";
  $attr=array('samaccountname','telephoneNumber','facsimileTelephoneNumber', 'userPrincipalName');
  if($result=ldap_search($ldap['dc'], $ldap['dn'], $filter, $attr)){
    $info = ldap_get_entries($ldap['dc'], $result);
    for($i=0; $i<$info['count']; $i++){
      $num=$info[$i]['telephonenumber'][0];
      $login=$info[$i]['samaccountname'][0];
      $login=strtoupper( substr($login,0,1) ).substr($login,1);
      $context=(array_key_exists('facsimiletelephonenumber', $info[$i]))?$info[$i]['facsimiletelephonenumber'][0]:'users';
      $grp=$num[0];
      $mail=$info[$i]['userprincipalname'][0];
      $out.="
[$num]({$conf['tpl']})
fullname = $login
secret = *** ; generate your super password here
email = $mail
callgroup = $grp
pickupgroup = $grp
context = $context
";

    }
  } else $error="Unable to search ldap server<br>msg:'".ldap_error($ldap['dc'])."'</br>";
} else $error="LDAP bind failed!";

if($error || !$out) {
  mail($conf['mail'], 'LDAP sync error!', $error);
  $out=file_get_contents($cache);
} else file_put_contents($cache,$out);

echo $out;
?>
As you see, we use AD field 'facsimileTelephoneNumber' for our DID numbers. Test this script - it must generate needed part of users.conf without template. It must work even without connection to LDAP server, read data from it's cache and send you error report by mail. (You need to make mail configured for this to work)
If script works as you need - include it to users.conf after SIP user template, like this
[lan](!)
type=peer
host=dynamic
...

#exec /etc/asterisk/ldap.php
And clear all your old extensions lines for SIP users, cuz this script wil generate them for you.
To make exec-includes work in asterisk you need to edit asterisk.conf too.
[options]
execincludes = yes; Support #exec in config files.

Test this by 'sip reload' and check by 'sip show peers' - you must get all your SIP users right from AD every time you make 'sip reload'.
And last step - add user sync run by schedule:
#crontab -e
  */10 8-20 *   *   *     /usr/sbin/asterisk -rx "sip reload"
Upper line means run every 10 minutes from 8am till 8pm every day.
Now your junior admins can manage users right from AD and not disturb you.

10 comments:

  1. А как сделать скрипт который просто будет получать имя звонящего на телефон с лдап. Просматривая внутренний номер ну и мобильный

    ReplyDelete
    Replies
    1. Есть 2 решения:
      1. Написать AGI скрипт, который будет перехватывать событие звонка и подставлять нужный clid
      2. Тут вопрос уже к телефонному аппарату, который этот номер собирается получить. Вроде у linksys есть возможность переходить по ссылке при звонке для получения номера, или загрузке адресной книги. Тут уже зависит от формата, который у каждого производителя свой. Т.е. нужно переделать, например, PHP-скрипт выше так, чтобы он отдавал адреску в нужном формате и вывесить его через апач, а на телефоне настроить путь к скрипту.

      Delete
  2. Ну собственно телефон Grandstream gxp 1405 пока станция не вылетела и не потерялся весь функционал. То в настройка конфиг файла для телефона была сылка на тел книгу вида, http://ххх.хх.ххх.х/gs_phonebook.php
    Ну сам скрипт утерян , вот и думаю как лучше восстановить функционал. Находил вариант через ldapsearch shell ( perl скрипт ) но что-то не пашет , да и в контролере информацию надо конвертить в UTF

    ReplyDelete
  3. Спасибо за скрипт, но у меня появился один вопрос. У меня возникла проблема фильтром по нестандартных полям LDAP (нужно экспортировать аккаунты из Zimbra). К примеру мы хотим хранить пароли пользователей в поле zimbraNotes, но скрипт напрочь отказывается видеть это поле. filter и attr правил соответствующим образом:
    $filter = "(&(telephoneNumber=*)(objectClass=zimbraAccount)(uid=*))";
    $attr=array('cn','telephoneNumber','facsimileTelephoneNumber','mail','zimbraNotes');
    Через ldapsearch фильтр работает правильно.

    Возможно php5-ldap не умеет вычитывать нестандартные поля?

    ReplyDelete
    Replies
    1. Ситуацию негде сэмулировать, но я бы проверял так:
      1. удалил из $filter часть с zimbraAccount и посмотрел будет ли вывод от ldap_search
      2. удалить zimbraNotes из $attr и посмотреть есть ли вывод у ldap_search. Если есть - то возможно атрибут надо писать регистрозависимо
      3. В зависимости от результата 1 и 2 копать решение дальше :)

      Delete
    2. собственно есть оставить только в $filter - вывод пустой (вроде как нет записей, попадающих под фильтр)
      если оставляю только в $attr - дальше получаю ошибку мол Undefined index: zimbranotes (при любом регистре)

      Delete
    3. такое такое впечатление, что php5-ldap просто игнорирует поля, которых нет в стандартных схемах.

      Delete
    4. Да вроде должно работать
      http://php.net/manual/en/function.ldap-search.php
      ничего не сказано про extended atributes
      При этом по (google "ldap_search" zimbranotes)
      первая ссылка
      http://www.zimbra.com/forums/developers/10287-ldap-attributes-different-output-perl-php.html
      см. последнюю цитату топикстартера

      Delete
    5. Спасибо за наводку.
      Оказалось все довольно банально, дополнительные поля, которые находятся в LDAP зимбры может читать только специальный пользователь, а я хотел их вычитать через обыкновенного юзверя.

      Delete
  4. Is it possible to use also Active Directory password in Asterisk SIP authentication?

    ReplyDelete