Для компаний, использующих телефоны 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-адреса телефона.

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
<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 без учета регистра
Поделиться с друзьями
-->

Комментарии (15)


  1. ARMADIK
    13.01.2017 11:59

    Спасибо за статью полезна при большом количестве цысок, про ускорение загрузки полезный совет!
    Думаю данный процесс можно запилить и под другие вендоры (snow,astra...).


    1. navion
      13.01.2017 13:31

      За других вендоров не скажу, но у Grandstream всё несколько проще и урл с конфигами задаётся в DHCP Option 66.


      1. kab01m
        16.01.2017 11:25

        Так урл с конфигом итак понятен без всяких option. Модель непонятна, о том и статья.


        1. navion
          16.01.2017 12:24

          Модель должна быть в Option 60, а дальше прямо с DHCP-сервера через Option 43 можно отдавать разные папки в урле.


          1. kab01m
            16.01.2017 14:43

            О, а это идея! Тогда не нужно хранилище связки мак-модель. Спасибо за подсказку, я подумаю.


    1. kab01m
      16.01.2017 16:00

      Кстати ускорение больше всего заметно на выборе обоев — скачать List.xml и все тумбнейлы телефону становится намного проще ;)

      У 7945 есть проблемы с этим — по tftp при количестве картинок ~20 он может повиснуть, а на http все ништяк.


  1. PegasusDuh
    16.01.2017 11:23
    +1

    На сколько помню, цискофоны могут использовать опцию 160. В ней можно указать сервер с конфигой вместе с протоколом (http://server.name:port/bla-bla-bla). А дальше хоть LDAP, хоть SQL, хоть что. Поищите cisco autoprovisioning.
    Собственно у меня так и сделано. И если аппарат ещё ни разу в сети не был, то ему по шаблону создается конфига и отдается, а астеру добавляется новый пир. Таким образом технику достаточно взять любой аппарат из коробки и воткнуть его в сеть — через 2-3 минуты он готов к работе.
    Если закупили другие аппараты, то добавляется модуль под эту модель, а вся логика остается прежней. Уже лет пять пользуюсь — техники довольны как слоны.


    1. kab01m
      16.01.2017 11:24

      Речь же шла об определении модели, а как вы с этим справились?


      1. PegasusDuh
        16.01.2017 11:47
        +1

        Первое — разные модели по-умолчанию запрашивают файл по разным путям (http://server:port/Cisco/SPA/model — как-то так, я пользовался сниффером и http-access.log, другие вендоры/модели соответственно по другому пути). Второе — можно парсить user agent, но тут вопрос не изменится ли он после обновления прошивки.Третье — как вариант я думал в ответ на запрос файла дергать что-то с клиента (можно его текущую конфигу, в ней есть модель), но тут опять же вопрос независимости от прошивки, ну и наверно не с любого аппарата можно что-то дернуть (есть же разные китайцы и т.п.).


        1. kab01m
          16.01.2017 14:49

          В том и фишка — разные пути спрашивает Linksys, а ентерпрайз циски спрашивают SEP.xml, потом XMLdefault.xml и все. И в user-agent у них ничего нет. В буквальном смысле — даже поля такого нет. А дергать клиента тоже не получится — пока конфиг не получен — сервисы не подняты. Потому и предложен подобный механизм.


          1. PegasusDuh
            16.01.2017 15:14

            Это потому-что вы используете TFTP, в HTTP все есть.
            А аппараты у меня Cisco — SPA 112,122,303,502 и некоторое количество китайцев, взятых на тесты.
            На предыдущем месте работы у меня тоже были аппараты которые работали с CCM по протоколу SCCP (если память не изменяет). Их я перешивал на SIP.


  1. brestows
    16.01.2017 14:21

    У меня вопрос, как человека у которого не одна сотня телефонов разбросанная по офисам, какой смысл в постоянной генерации конфиг файла, если проще один раз его сформировать и забыть? Изменение данных sip или пользовательских происходят редко, не каждый же день вы меняете фио сотруднику или регистрационные данные. У меня система работает следующим образом, свичи уведомляют об изменениях состояния портов (snmp), snmp сервер запускает скрипт, скрипт проверяет наличие конфиг файла для конкретного мака, если конфига нет он генерирует новый, тем самым выдает телефону новый номер, админу остается только зайти в web панель, найти только что сконфигурированый конфиг и привязать его к сотруднику, на этом настройка заканчивается. В случае изменения данных о сотруднике либо если необходимо донастроить телефон, вторая линия внешняя или еще что, админ просто заходит на web морду и редактирует конфигурационный файл пользователя, выглядит это примерно так:



    Ваш подход довольно интересен и полезен для тех у кого много Cisco хотя не думаю что как-то зависим Ваш алгоритм от вендора телефонов


    1. kab01m
      16.01.2017 14:38

      У меня лично следующие аргументы:

      Представим ситуацию — купили мы недавно 8845. Его — руководителю, телефон руководителя в переговорную, телефон из переговорной менеджеру, телефон менеджера — на помойку. На телефоне должна быть фамилия и актуальный номер человека. Это задается в конфиге, а конфиг получается персональным, значит динамическим. У вас — web, у меня LDAP, всего и разницы.

      Или вот вторая ситуация — залили мы 9.4.2 в 8941, а он глючный, прошивку всем откатить надо. Cisco выпустила SR3, попробовали, вроде работает — накатить. Нет, все же проблемная — откатить.

      От вендора не зависит, однако хочу заметить что такая проблема как узнать марку и подсунуть правильный конфиг актуальна только на циско и поликомах (из тех что у меня). Linksys сразу запрашивает общий конфиг типа spa901.cfg, например.


      1. brestows
        16.01.2017 15:15

        Ну тут я с вами согласен, разницы чем и как редактировать особой нет. Если у Cisco так сильно разница конфиг телефонов от модели к модели, возможно Ваш вариант более подходящий, хотя и так и так требует ручного вмешательства, вот если бы оно само :-D


        1. kab01m
          16.01.2017 16:06

          Ну, на прошивке 9.х все нормализовалось — конфиг один и тот же для всей серии 79xx, 89xx, 88xx, что не может не радовать.