Как легко и просто научить ваш Asterisk звонить через нужного оператора

Приветствую тебя, %username%!

Сегодня мне хотелось бы поделиться решением, позволяющим научить ваш Asterisk автоматически маршрутизировать звонки по соответствующим направлениям, не прибегая к громоздким регулярным выражениям.

image

Практика показывает, что все больше и больше компаний начинают задумываться о своих расходах на связь. Львиную часть расходов при этом составляют вызовы на мобильные номера. Отсюда и родилась задача обрабатывать исходящие вызовы и направлять через ту линию, где звонок будет совершен бесплатно или за наименьшую стоимость.

В моем случае, необходимо было маршутизировать звонки на домашний регион операторов большой тройки через соответствующие сим-карты, вставленные в GSM-шлюз. Вместо выбранных операторов можно использовать любые другие направления по аналогии с сабжем.

В качестве шлюза выступал OpenVox VoxStack VS-GW1202-4G, в который предварительно были вставлены сим-карты известных вам операторов и настроены 3 транка до Asterisk.

Безусловно, путей решения проблемы великое множество, но мне хотелось бы остановиться на решении, которое я счел оптимальным: автоматическая сверка номеров из базы MySQL и дальнейшее перенаправление на нужную линию. Реализация довольно проста, хотя местами может показаться неказистой.

Разбить все работы можно на несколько этапов:
  1. Создать и заполнить базу данных
  2. Подготовить Asterisk к работе с БД
  3. Написать диалплан для обработки исходящих вызовов


Подготовка базы



Для начала необходимо создать отдельную базу (опционально):

CREATE DATABASE telcodes;


Рутовый доступ в нашей ситуации лучше не давать, т.к. в этом нет необходимости, да и дырка в безопасности в виде рутового логина и пароля к MySQL в открытом виде в конфигурационном файле никому не нужна. Поэтому даем доступ произвольному пользователю только к нашей БД (в моем случае пользователь: «mobile», пароль: «heavypass»)

CREATE DATABASE telcodes;
GRANT ALL ON telcodes.* TO mobile@localhost IDENTIFIED BY 'mobile';
FLUSH PRIVILEGES;
SET PASSWORD FOR 'mobile'@'localhost'=PASSWORD('heavypass');


Создаем таблицу:
Скрытый текст
CREATE TABLE telcodes ( 
code smallint(3) NOT NULL,
begin int(11) NOT NULL,
end int(11) NOT NULL,
ammount int (11) NOT NULL,
operator varchar(100) NOT NULL,
region varchar(100) NOT NULL) 
DEFAULT CHARSET=utf8;


А теперь можно перейти непосредственно к заполнению. Каждый волен сам выбирать для себя способ, я остановлюсь на следующем:

wget http://www.rossvyaz.ru/docs/articles/Kody_DEF-9kh.csv
cat Kody_DEF-9kh.csv | iconv -f pt154 -t utf8  >> telcodes.csv
mysqlimport -umobile -p telcodes /usr/src/telcodes.csv --fields-terminated-by=';' --lines-terminated-by="\r\n"

Выкачиваем список кодов мобильных операторов с сайта Россвязи, преобразуем кодировку и заливаем в нашу базу. Путь до файла опционален.

База заполнена, но чтобы избежать проблем с кодировками внесем небольшие изменения: т.к. мне необходимо отбирать номера большой тройки для звонков в домашнем регионе, то я меняю только их, регион помечаю как home, а названия меняю на beeline, mts и megafon соответственно:

UPDATE telcodes set region = 'home',operator = 'beeline' where operator = 'Вымпел-Коммуникации' and region = 'Москва и Московская область';
UPDATE telcodes set region = 'home',operator = 'mts' where operator = 'Мобильные ТелеСистемы' and region = 'Москва и Московская область';
UPDATE telcodes set region = 'home',operator = 'megafon' where operator = 'МегаФон' and region = 'Москва и Московская область';


База данных готова к использованию.

Подготовка asterisk



Соединять Asterisk с БД будем при помощи ODBC, т.к. считаю это унифицированным решением. Для начала необходимо пересобрать Asterisk с поддержкой ODBC. В качесте ОС в моем случае выступает Ubuntu Server 12.04.

Ставим пакеты unixodbc, unixodbc-dev и libmyodbc

apt-get install unixodbc unixodbc-dev libmyodbc


В каталоге с исходниками Asterisk выполняем:
./configure && make menuselect


Проверяем что модуль res_odbc доступен и включен, в функциях надо проверить доступность func_odbc. Затем сохраняем и выполняем:

make && make install


Смотрим где лежат драйвера для ODBC
# dpkg -L libmyodbc 

Скрытый текст
/.
/usr
/usr/lib
/usr/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu/odbc
/usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so                            #Запоминаем месторасположение
/usr/share
/usr/share/libmyodbc
/usr/share/libmyodbc/odbcinst.ini
/usr/share/doc
/usr/share/doc/libmyodbc
/usr/share/doc/libmyodbc/README.Debian
/usr/share/doc/libmyodbc/changelog.Debian.gz
/usr/share/doc/libmyodbc/examples
/usr/share/doc/libmyodbc/examples/odbc.ini
/usr/share/doc/libmyodbc/copyright



Смотрим где лежат конфигурационные файлики для ODBC
#odbcinst -j

Скрытый текст
unixODBC 2.2.14
DRIVERS............: /etc/odbcinst.ini
SYSTEM DATA SOURCES: /etc/odbc.ini
FILE DATA SOURCES..: /etc/ODBCDataSources
USER DATA SOURCES..: /root/.odbc.ini
SQLULEN Size.......: 8
SQLLEN Size........: 8
SQLSETPOSIROW Size.: 8



Правим /etc/odbcinst.ini, добавляем в файл запись:
Скрытый текст
[MySQL]
Description = MySQL driver
Driver = /usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so 			#указываем запомненное местоположение
Setup = /usr/lib/x86_64-linux-gnu/odbc/libodbcmyS.so			#там же лежит файл libodbcmyS.so
CPTimeout =
CPReuse =



Описываем подключение к БД в файлe /etc/odbc.ini
Скрытый текст
[telcodes-mysql]
Driver = MySQL
Description = MySQL telcodes via ODBC
Server = localhost				#адрес сервера, где стоит база
Port = 3306
User = mobile					#Созданный ранее пользователь
Password = heavypass			#Пароль пользователя
Database = telcodes		        #База данных, где будут храниться
Socket = /var/run/mysqld/mysqld.sock



А теперь непосредственно знакомим Asterisk с MySQL при помощи ODBC. Вносим изменения в /etc/asterisk/res_odbc.ini
Скрытый текст
[telcodes]
enabled => yes
dsn => telcodes-mysql
username => mobile
password => heavypass
pooling => no
limit => 1
pre-connect => yes



и прописываем загрузку модуля в /etc/asterisk/modules.conf
preload => res_odbc.so


Теперь можно перезапустить сам Asterisk, ну или только загрузить модуль res_odbc.so. После чего в CLI выполняем команду:
odbc show all

Скрытый текст
aster*CLI> odbc show all

ODBC DSN Settings
— Name: telcodes
DSN: telcodes-mysql
Last connection attempt: 1970-01-01 03:00:00
Pooled: No
Connected: Yes


На этом второй этап можно считать завершенным, мы создали и заполнили БД и подружили её с Asterisk.

Написание диалплана



Ну а теперь можно приступить непосредственно к написанию диалплана для обработки исходящих вызовов.

Каждый набранный номер обрабатывается макросом, в результе работы которого будет определена линия, через которую будет совершен вызов. В качестве аргумента передаваемого макросу будет выступать набираемый номер. Первый символ (8) будет отрезаться еще до попадания в макрос, после чего в SQL запросе мы будем отбирать дальнейшие первые 3 символа (123) для сравнения с кодом оператора и оставшиеся 7 символов (4567890) для определения принадлежности к той или иной номерной емкости.

Все работы с БД прописываются в конфигурационном файле func_odbc.conf. Здесь мы придумываем и составляем функции для взаимодействия с БД, а затем используем их результат в диалплане. Каждый раздел описывает свою функцию, название каждого раздела принято заполнять в верхнем регистре.
В моем случае необходимо определить принадлежность номера к региону и оператору и передать результат для дальнейшей обработки макросом.

Добавляем в /etc/asterisk/func_odbc.conf запись
[MOBILE]
dsn=telcodes-mysql
read=SELECT operator,region FROM `telcodes` WHERE ${ARG1:3:9} BETWEEN `begin` AND `end` AND `code`=${ARG1:0:3}

Стоит запомнить, что результатом запроса будут служить 2 значения, разделенных запятой: оператор, обслуживающий данный номер, а так же регион обслуживания.

Ну а теперь можно непосредственно написать сами правила обработки номера. В примере буду опускать лишние правила, оставлю только то, что относится к делу. Все изменения производим в файле /etc/asterisk/extensions.conf

Создаем контекст для исходящих звонков.
Скрытый текст
[from-users]

exten => _8XXXXX.,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN})
same => n,Set(TARGETNO=${EXTEN})
same => n,Macro(seeknumber-odbc,${EXTEN:1:10})
same => n,Noop(${TRUNK_MOB})
same => n,Goto(to-${TRUNK_MOB},${EXTEN},1)
same => n,Hangup



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

Скрытый текст
[to-prov-trunk]
exten => _8XXXXX.,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN} other)
same => n,Dial(SIP/prov-trunk/${EXTEN},140,Tt)
same => n,Hangup()

[to-beeline]
exten => _8XXXXX.,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN} beeline)
same => n,Dial(SIP/beeline/${EXTEN},140,Tt)
same => n,Goto(status,s-${DIALSTATUS},1)

[to-megafon]
exten => _8XXXXX.,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN} megafon)
same => n,Dial(SIP/megafon/${EXTEN},140,Tt)
same => n,Goto(status,s-${DIALSTATUS},1)

[to-mts]
exten => _8XXXXX.,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN} mts)
same => n,Dial(SIP/mts/${EXTEN},140,Tt)
same => n,Goto(status,s-${DIALSTATUS},1)

[status]
exten => s-ANSWER,1,Hangup()
exten => s-CHANUNAVAIL,1,Goto(to-prov-trunk,${TARGETNO},1)
exten => s-BUSY,1,Goto(to-prov-trunk,${TARGETNO},1)
exten => s-.,1,Hangup()



Ну и теперь непосредственно сам макрос:
Скрытый текст
[macro-seeknumber-odbc]
exten => s,1,NoOp( == tel nomber == ${ARG1:0:3} == ${ARG1:3:9} == )
same => n,Set(result=${ODBC_MOBILE()})
same => n,Set(operator=${CUT(result,\,,1)})
same => n,Set(region=${CUT(result,\,,2)})
same => n,NoOp(name of region = ${region})
same => n,NoOp(name of operator = ${operator})
same => n,Set(TRUNK_MOB=prov-trunk)
same => n,ExecIf($["${operator}"=«beeline»&"${region}"=«home»]?Set(TRUNK_MOB=beeline))
same => n,ExecIf($["${operator}"=«mts»&"${region}"=«home»]?Set(TRUNK_MOB=mts))
same => n,ExecIf($["${operator}"=«megafon»&"${region}"=«home»]?Set(TRUNK_MOB=megafon))


В итоге получилось, на мой взгляд, неплохое унифицированное решение для распределения вызовов на нужные направления по номеру телефона. В дополнение скажу, что его можно автоматизировать еще больше, написав bash-скрипт на загрузку и обработку свежих баз с сайта РосСвязи и добавив его в crontab для ежемесячного апгрейда, но это будем считать домашним заданием ;)
  • +16
  • 28.4k
  • 9
Share post

Comments 9

    +2
    Зачем по сто раз в каждой статье писать как установить Астериск? Этой информации в интернете с головой, в том числе и на официальном wiki.

    По DIALPLAN зачет, только макрос — это громозкое решение, которое вскоре будет depricated.
    Если на то уж пошло то можно использовать GoSub, а еще лучше Lua. Там вообще все правильно можно сделать, без вот этих костылей с макросами. И работать это будет быстрее
      +2
      Ну а почему бы и не писать? Те, кто знают что к чему, просто пропустят эту информацию. Те, кто ищет описание решения от и до, скажут спасибо за развернутую информацию.

      Касательно макросов вынужден согласиться, но решение легко адаптирвать под GoSub.
        +3
        Более того, рекомендация по установке дана во вреднючем стиле:
        make && make install
        

        Уже есть и пост по этой проблеме и мой комментарий с предложением вместо хотя бы указывать make собирать параллельно:
        make -j [количество процессов]
        


        А костыль бережно переносится из руководство в руководство и конца этому не видно.
        +2
        Для целей определения мобильного оператора корректнее использовать базу перенесенных номеров MNP.
        При исходящем звонке запрашивать и кэшировать оператора и даже регион, можно с сайта мегафона moscow.shop.megafon.ru/get_ajax_page.php?action=getMsisdnInfo&msisdn=79000000000 или с теле2 mnp.tele2.ru/gateway.php?9000000000

        Если нужно список кодов мобильных операторов www.rossvyaz.ru/docs/MNC_25.11.2013-1.rtf
        и список кодов регионов (он не совпадает с автомобильными) www.rossvyaz.ru/docs/Identifikator_regiona.doc

        У меня как-то так получается /var/lib/asterisk/agi-bin/mnp:
        #!/bin/bash
        
        declare -a array
        while read -e ARG && [ "$ARG" ] ; do
                array=(` echo $ARG | sed -e 's/://'`)
                export ${array[0]}=${array[1]}
        done
        
        checkresults() {
                while read line
                do
                case ${line:0:4} in
                "200 " ) echo $line >&2
                         return;;
                "510 " ) echo $line >&2
                         return;;
                "520 " ) echo $line >&2
                         return;;
                *      ) echo $line >&2;;       #keep on reading those Invlid command
                                                #command syntax until "520 End ..."
                esac
                done
        }
        
        echo "DATABASE GET MNP $agi_extension"
        read line
        if [[ ${line:0:14} != "200 result=1 (" ]]; then
            MNP=$(/usr/bin/wget -q "http://mnp.tele2.ru/gateway.php?${agi_extension:1}" -O - | /usr/local/bin/jq -r '.response.geocode.code+.response.mnc.code')
            [[ -n $MNP ]] && echo DATABASE PUT MNP $agi_extension \"$MNP\" && checkresults
        fi
        
        exit 0;
        


        и в extensions.ael
        if (${DB_EXISTS(MNP/${EXTEN})} = 0)
        {
                AGI(mnp);
        }
        
        if ("${DB(MNP/${EXTEN}):2}" = "02" && ${DB(balance/multifon)} > 100)
        {
                if (${GROUP_COUNT(megafon)} < 5) {
                        GROUP()=megafon;
                        Dial(SIP/megafon/${EXTEN},90);
                        if (${DIALSTATUS} = BUSY) Busy();
                };
        }
        


        для простоты результаты кэшируем в бд самого астериска: первые 2 цифры — регион, вторые 2 цифры — код оператора

        (ессно если нужно только определить регион абонента, то база rossvyaz работает намного лучше, надежнее, быстрее и тд)
          +1
          Привязывать такой запрос непосредственно к интернету чревато подвисанием в ситуации, когда выход в ТФОП осуществляется независимо от доступа в интернет (по E1 например), а доступ в интернет отвалился или тормозит.
          +1
          С учетом возможности перехода от оператора к оператору с сохранением номера в обозримом будущем может стать не актуальным.
            +2
            Думаю, что большая часть емкостей останется на месте.
              0
              Внутри региона все равно номер остается без изменения. А между регионам MNP не работает.
              Мы в этой связи сделали для наших клиентов отдельный тарифный план — единая цена на звонки на мобильные ВСЕЙ России. Чтобы не было таких мучений.
              0
              На основе той же базы Россвязи есть модуль под node.js numcap для поиска региона и оператора по номеру телефона
              github.com/antirek/numcap

              Only users with full accounts can post comments. Log in, please.