Pull to refresh

Авто-генерация конфигурационных файлов телефонов Cisco

Reading time 6 min
Views 10K
Для компаний, использующих телефоны 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 без учета регистра
Tags:
Hubs:
+7
Comments 16
Comments Comments 16

Articles