Для компаний, использующих телефоны Cisco в среде Asterisk, существует проблема хранения десятков или сотен конфигурационных файлов для каждого телефона. На волне необходимости обновления 30 телефонов (частично по прошивкам, частично по настройкам) я решил предложить технологию автоматической генерации конфигурационных файлов.
Сразу надо сказать, что речь идет скорее о технологии, чем о конкретной реализации — код еще сырой и плохо отлажен. В этой статье предполагается, что вы уже конфигурировали телефоны 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-адреса телефона.
#!/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>
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 без учета регистра
Автор: kab01m