Для компаний, использующих телефоны Cisco в среде Asterisk, существует проблема хранения десятков или сотен конфигурационных файлов для каждого телефона. На волне необходимости обновления 30 телефонов (частично по прошивкам, частично по настройкам) я решил предложить технологию автоматической генерации конфигурационных файлов.
Сразу надо сказать, что речь идет скорее о технологии, чем о конкретной реализации — код еще сырой и плохо отлажен. В этой статье предполагается, что вы уже конфигурировали телефоны Cisco и знакомы с принципом их работы, как это например предлагается на voip-info.org и подобных ресурсах.
Итак, немного установочных данных:
Цискофоны бывают многих видов, но здесь речь идет о телефонах, работающих по технологии Cisco Call Manager (CCM) и только. Почему именно так — они самые приятные в использовании как с пользовательской, так и с админской стороны.
Обычный процесс загрузки и работы телефона с момента включения выглядит так:
Конфигурационный файл достаточно большой и сложный, определяет практически все аспекты работы телефона, что и вызывает определенный интерес.
Предположим TFTP-сервер это наше собственное ПО, тогда в момент отдачи конфигурации TFTP-сервер мог бы ее сгенерировать, если бы знал установочные параметры. В минимальном варианте список параметров таков:
Все это конечно есть в наших каталогах кроме прошивки.
Чтобы узнать что за прошивку надо предложить — тоже надо знать марку. Марку свою телефон говорит в двух случаях — в поле 60 DHCP запроса и при регистрации SIP. При регистрации это уже поздно, значит мы будем брать марку из DHCP-запроса.
В dhcpd.conf можно прописать следующее:
Это скажет DHCP-серверу, чтобы при выдаче lease он вызывал скрипт примерно такого вида:
Здесь нам надо где-то сохранить марку, чтобы при запросе конфигурации на нее опереться. Хранить ее можно где угодно — в БД, в БД Asterisk или текстовом файле. Я выбрал вариант хранить ее в LDAP наряду с другими учетными данными и использую для этого ненужный атрибут telexNumber. Чтобы назначать людям телефоны я добавил им класс ieee802device и приписал атрибут macAddress в виде MAC-адреса телефона.
Скрипт ищет в LDAP пользователя с мак-адресом, который сообщил dhcpd и проставляет ему telexNumber, если он еще не проставлен.
Интересно, что цискофоны сначала обращаются на сервер TFTP по порту 6970, протокол HTTP, а уже затем идут как обычно. Очень рекомендую заменить у себя в компании сервер TFTP на HTTP/6970 — загрузка телефона ускорится. Важно! Если сервер ответит, но вернет 404 или 500 — TFTP запрашиваться не будет и телефон не загрузится. TFTP нормально работает, если 6970 не отвечает вообще. Хуже всего, если порт заблокирован — загрузка замедляется в разы.
Таким образом картина следующая — телефон запрашивает свой файл SEP<MAC>, выдираем отсюда мак-адрес, спрашиваем в LDAP марку и другие параметры для телефона, открываем шаблон вида template.7941.xml, меняем в нем переменные и отдаем телефону. Если это не xml — файл отдаст сам Apache, если это xml но не для нас — отдаем его сами, если мак адрес или шаблон не найден — телефону 404.
Вызов функции _getLoadA можно заменить на _getLoadB и прошивки будут сами искаться в каталоге, в первом же случае их названия статично вбиты в код.
Ранее мне хотелось сделать сложные шаблоны из общих частей и так далее, но сейчас я такой необходимости не вижу и для всех телефонов кроме 3951 шаблон один.
Теперь достаточно просто расставить пользователям в LDAP MAC-адреса телефонов и все.
3. Опасности
Сразу надо сказать, что речь идет скорее о технологии, чем о конкретной реализации — код еще сырой и плохо отлажен. В этой статье предполагается, что вы уже конфигурировали телефоны Cisco и знакомы с принципом их работы, как это например предлагается на voip-info.org и подобных ресурсах.
Итак, немного установочных данных:
Цискофоны бывают многих видов, но здесь речь идет о телефонах, работающих по технологии Cisco Call Manager (CCM) и только. Почему именно так — они самые приятные в использовании как с пользовательской, так и с админской стороны.
Обычный процесс загрузки и работы телефона с момента включения выглядит так:
- получение IP-адреса;
- скачивание конфигурационного файла с TFTP-сервера (с названием вида SEP<MAC>.cnf.xml;
- скачивание разных файлов, упомянутых в конфигурации, в т.ч. прошивки;
- регистрация в Asterisk.
Конфигурационный файл достаточно большой и сложный, определяет практически все аспекты работы телефона, что и вызывает определенный интерес.
Предположим TFTP-сервер это наше собственное ПО, тогда в момент отдачи конфигурации TFTP-сервер мог бы ее сгенерировать, если бы знал установочные параметры. В минимальном варианте список параметров таков:
- логин/пароль устройства в Asterisk;
- фамилию сотрудника у которого стоит телефон;
- название файла прошивки телефона и марку телефона, если конфигурации будут отличаться.
Все это конечно есть в наших каталогах кроме прошивки.
1. Получение IP-адреса
Чтобы узнать что за прошивку надо предложить — тоже надо знать марку. Марку свою телефон говорит в двух случаях — в поле 60 DHCP запроса и при регистрации SIP. При регистрации это уже поздно, значит мы будем брать марку из DHCP-запроса.
В dhcpd.conf можно прописать следующее:
on commit { set clip = binary-to-ascii(10, 8, ".", leased-address); set clhw = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); execute("/root/bin/dhcpevent.php", "commit", clip, clhw, option vendor-class-identifier); }
Это скажет DHCP-серверу, чтобы при выдаче lease он вызывал скрипт примерно такого вида:
/root/bin/dhcpevent.php commit 172.20.21.209 0:f:77:12:bc:aa "Cisco Systems, Inc. IP Phone CP-7945G"
Здесь нам надо где-то сохранить марку, чтобы при запросе конфигурации на нее опереться. Хранить ее можно где угодно — в БД, в БД Asterisk или текстовом файле. Я выбрал вариант хранить ее в LDAP наряду с другими учетными данными и использую для этого ненужный атрибут telexNumber. Чтобы назначать людям телефоны я добавил им класс ieee802device и приписал атрибут macAddress в виде MAC-адреса телефона.
dhcpevent.php
#!/usr/local/bin/php <?php $rdn = 'uid=root,ou=Users,dc=labma,dc=ru'; // DN to auth against LDAP $pass = 'superpass'; // Password $cont = "telexNumber"; // Attribute to fill with Cisco phone ID $ds = ldap_connect("pilot.labma.ru"); // Exit if not connected if (!$ds) exit (128); // Modern LDAP do not work on v1/v2 if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) exit (128); // That means phone is not for us if (!preg_match ("/^Cisco/", $argv[4])) exit (1); $r = ldap_bind($ds, $rdn, $pass); $mac = ""; $macar = explode (":", $argv[3]); if (count($macar) != 6) exit (128); // PHP LDAP client get keys in low $contl = strtolower($cont); // DHCP server send not padded MAC foreach ($macar as $byte) $mac .= str_pad ($byte, 2, 0, STR_PAD_LEFT); $sr = ldap_search( $ds, "dc=labma, dc=ru", "macAddress=$mac", array ("dn", $cont) ); if (ldap_count_entries($ds, $sr) != 1) exit (4); $info = ldap_get_entries($ds, $sr)[0]; if ((array_key_exists($contl, $info)) && ($argv[4] == $info[$contl][0])) exit (0); $res = ldap_mod_replace ( $ds, $info["dn"], array ($cont => $argv[4]) ); if (!$res) exit (128); ldap_close ($ds); exit (0); ?>
Скрипт ищет в LDAP пользователя с мак-адресом, который сообщил dhcpd и проставляет ему telexNumber, если он еще не проставлен.
2. Получение конфигурации
Интересно, что цискофоны сначала обращаются на сервер TFTP по порту 6970, протокол HTTP, а уже затем идут как обычно. Очень рекомендую заменить у себя в компании сервер TFTP на HTTP/6970 — загрузка телефона ускорится. Важно! Если сервер ответит, но вернет 404 или 500 — TFTP запрашиваться не будет и телефон не загрузится. TFTP нормально работает, если 6970 не отвечает вообще. Хуже всего, если порт заблокирован — загрузка замедляется в разы.
Делаем VirtualHost *:6970
и .htaccess:
А в /export/tftp кладем собственно index.php
<VirtualHost *:6970> ServerAdmin webmaster@pbx.labma.ru DocumentRoot "/export/tftp" </VirtualHost> <Directory "/export/tftp/"> Options Indexes FollowSymLinks AllowOverride None Require ip 172.20.21.0/24 </Directory>
и .htaccess:
RewriteEngine On RewriteRule ^(.*)\.xml$ index.php [L]
А в /export/tftp кладем собственно index.php
<?php if (preg_match ("/\SEP(\w+).cnf.xml/", $_SERVER["REQUEST_URI"], $m)) $mac = $m[1]; else { $file = getcwd ().$_SERVER["REQUEST_URI"]; if (!file_exists ($file)) _fail(); header ("Content-type: text/xml"); header ('Content-Length: ' . filesize($file)); readfile ($file); exit (0); } $user = _getUser($mac); if (!$user) _fail(); $tmpl = "template.".$user["cisco"].".xml"; if (!file_exists ($tmpl)) _fail (); $xml = file_get_contents ("template.".$user["cisco"].".xml"); // getLoadA hardcoded, loadB - search directory $user["load"] = _getLoadA($user["cisco"]); foreach ($user as $key => $value) { $xml = preg_replace ("/\#\#$key\#\#/m", $value, $xml); } header ("Content-type: text/xml"); header ('Content-Length: ' . strlen($xml)); echo $xml; exit; function _getUser ($mac) { $rdn = 'uid=root,ou=Users,dc=labma,dc=ru'; // DN to auth against LDAP $pass = 'superpassword'; // Password $cont = "telexNumber"; // Attribute to fill with Cisco phone ID $ds = ldap_connect("pilot.labma.ru"); // Exit if not connected if (!$ds) exit (128); // Modern LDAP do not work on v1/v2 if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) exit (128); $r = ldap_bind($ds, $rdn, $pass); $sr = ldap_search( $ds, "dc=labma, dc=ru", "macAddress=$mac" ); if (ldap_count_entries($ds, $sr) != 1) { return null; } $info = ldap_get_entries($ds, $sr)[0]; $user = array(); $user ["label"] = $info["sn"][0]; $user ["phone"] = $info["uidnumber"][0]; if (preg_match ("/CP-(\d+)/", $info["telexnumber"][0], $m)) $user ["cisco"] = $m[1]; else return null; return $user; } function _getLoadA($cisco) { $list = array ( 3951 => "SIP3951.8-1-4a", 7906 => "SIP11.9-4-2SR1-1S", 7911 => "SIP11.9-4-2SR1-1S", 7931 => "SIP31.9-4-2SR2-2S", 7941 => "SIP41.9-4-2SR2-2S", 7945 => "SIP45.9-4-2SR2-2S", 7961 => "SIP41.9-4-2SR2-2S", 7965 => "SIP45.9-4-2SR2-2S", 8941 => "SIP894x.9-4-2SR3-1", 8845 => "sip8845_65.11-5-1SR1-1", 8865 => "sip8845_65.11-5-1SR1-1", ); if (!array_key_exists ($cisco, $list)) return ""; if (!file_exists (getcwd()."/".$list["cisco"].".loads")) return ""; return $list[$cisco]; } function _getLoadB($cisco) { $list = array ( 3951 => "SIP3951", 7906 => "SIP11", 7911 => "SIP11", 7931 => "SIP31", 7941 => "SIP41", 7945 => "SIP45", 7961 => "SIP41", 7965 => "SIP45", 8941 => "SIP894x", 8845 => "sip8845_65", 8865 => "sip8845_65", ); if (!array_key_exists ($cisco, $list)) return ""; $files = glob ($list[$cisco].".*.loads"); if (count($files) != 1) return ""; else return str_replace (".loads", "", $files[0]); } function _fail () { header ("HTTP/1.0 404 Not Found"); exit (0); } ?>
Таким образом картина следующая — телефон запрашивает свой файл SEP<MAC>, выдираем отсюда мак-адрес, спрашиваем в LDAP марку и другие параметры для телефона, открываем шаблон вида template.7941.xml, меняем в нем переменные и отдаем телефону. Если это не xml — файл отдаст сам Apache, если это xml но не для нас — отдаем его сами, если мак адрес или шаблон не найден — телефону 404.
Вызов функции _getLoadA можно заменить на _getLoadB и прошивки будут сами искаться в каталоге, в первом же случае их названия статично вбиты в код.
Ранее мне хотелось сделать сложные шаблоны из общих частей и так далее, но сейчас я такой необходимости не вижу и для всех телефонов кроме 3951 шаблон один.
Теперь достаточно просто расставить пользователям в LDAP MAC-адреса телефонов и все.
3. Опасности
- Если порт 6970 внятно отвечает — TFTP запрашиваться не будет
- Телефоны с прошивкой SIP 8.x не запрашивают порт 6970
- TCP и UDP телефоны одновременно работать в одном Asterisk не могут — следите за прошивками и тэгом transportLayer
- DHCP-сервер отдает мак без незначащих нулей
- LDAP сервер ищет по MAC без учета регистра
