О чем собственно речь?

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

Есть у нас, к примеру, несколько (ну ладно, не несколько) однотипных филиальчиков на 10-100 сотрудников. Организовать телефонию можно разными путями:

  • Взять готовое решение в облаке провайдера

  • Поставить на каждый филиал свой мини-сервер и астериск

  • Сделать единый астериск в центре

Первые 2 варианта, конечно имеют место быть, но минусов у них хватает. Это и не удобство сопровождения нескольких малых инстантсов, это и финансовый вопрос и некоторые другие моменты.

Если же мы хотим оптимизации, экономии, упростить администрирование, то рано или поздно мы приходим к тому, что сервер в центре будет наиболее удачным решением. Но, как только мы попытаемся стандартизировать номерной план, мы столкнемся с проблемой плоской нумерации. Мы начинаем добавлять префиксы, наращивать длину номеров, и вот у нас уже не 3х значная нумерация, а 6ти значная. Сотрудники все чаще поглядывают косо - чтобы набрать соседа надо нажать 6 кнопок ("6 кнопок Карл! Я сюда не кнопки нажимать пришел!"), а заявление босса что компания еще немного выросла и появился еще один филиал приводит к судорожному перебиранию записей на листочке в поисках свободного префикса в 6ти значной нумерации.

Но как говорится, все уже придумано до нас - это SIP домены. К сожалению, не всегда эта тема так очевидна для администратора-универсала, и порой скрыта в глубинах англоязычных мануалов.

Попробуем разобраться, что это и как это настроить. Прежде всего, домен - это просто текстовая строка, но которую мы можем материализовать в виде днс-имени. А можем и не материализовать и оставить просто строкой. Как правильно? Ну как говорится, все сильно зависит от задачи - работать будет и так и так, но если много софтфонов, то лучше сделать ДНС - на одно поле меньше придется заполнять при настройке.

Тестировать будем астериск 18.1 со стеком PJSIP в связке с реалтайм базой данных.

Что мы будем хранить в базе? Конечно же наших любых юзверей, с их параметрами. Но мы чуть-чуть схитрим и отойдем от мануалов астериска. Не удобно вести те талмуды в базе данных, что предлагается документацией астериска "из коробки".

Поэтому начнем с создания удобной для нас таблички (Numbers), без оглядки на мануалы. И так, что мы должны знать об абоненте?

  • Конечно же, его номер (Number)

  • Пароль тоже нужен - вдруг это враг? (Secret)

  • Домен - ну раз тема про домен, очевидно, он тоже должен быть (domain)

  • CID - мы ж хотим чтобы он как то отображался при звонке

  • Опционально: протокол, DTMF, пикап-группа

  • Поле Flags - свои флаги-опции, которые станут канальными переменными. Адептов полного реалтайм просьба расслабиться, канальные переменные в разы быстрее реалтайма, поэтому такие флаги могут сильно сократить нагрузку на базу данных.

Будем считать, что табличка у нас есть. Что дальше? Ок, такую табличку астериск не прочитает, увы. Но у базы данных есть секретное оружие - вьюшки, старые добрые вьюшки, VIEW. Они позволяют взять данные, в удобном для нас виде, и представить их в удобном виде для астериска. Всего нам потребуется 3 шт:

  • Endpoints (ps_endpoints)

  • Авторизация (ps_auths)

  • Aors (ps_aors)

Это минимальный набор, по вкусу в это блюдо можно добавить и другие, для реалтайма или "волшебства" PJSIP. Позволю себе побыть лентяем и дам просто готовый код SQL:

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;

CREATE ALGORITHM=UNDEFINED SQL SECURITY DEFINER VIEW `ps_aors` AS select concat(`Numbers`.`Number`,if(`Numbers`.`domain` is not null,concat('@',`Numbers`.`domain`),'')) AS `id`,180 AS `default_expiration`,2 AS `max_contacts`,30 AS `minimum_expiration`,'yes' AS `remove_existing`,'' AS `contact` from `Numbers`;
CREATE ALGORITHM=UNDEFINED SQL SECURITY DEFINER VIEW `ps_auths` AS select concat(`Numbers`.`Number`,if(`Numbers`.`domain` is not null,concat('@',`Numbers`.`domain`),'')) AS `id`,'userpass' AS `auth_type`,3600 AS `nonce_lifetime`,`Numbers`.`Secret` AS `password`,`Numbers`.`Number` AS `username`,`Numbers`.`domain` AS `realm` from `Numbers` where `Numbers`.`Secret` <> '';
CREATE ALGORITHM=UNDEFINED SQL SECURITY DEFINER VIEW `ps_endpoints` AS select concat(`Numbers`.`Number`,if(`Numbers`.`domain` is not null,concat('@',`Numbers`.`domain`),'')) AS `id`,concat('transport-',lcase(`Numbers`.`Protocol`)) AS `transport`,concat(`Numbers`.`Number`,if(`Numbers`.`domain` is not null,concat('@',`Numbers`.`domain`),'')) AS `aors`,if(`Numbers`.`Secret` <> '',concat(`Numbers`.`Number`,if(`Numbers`.`domain` is not null,concat('@',`Numbers`.`domain`),'')),NULL) AS `auth`,'SIP' AS `context`,'all' AS `disallow`,concat('ulaw,alaw,opus',if(find_in_set('Video',`Numbers`.`Flags`),',h263,h261,h263p,h264','')) AS `allow`,concat(`Numbers`.`Number`,if(`Numbers`.`domain` is not null,concat('@',`Numbers`.`domain`),'')) AS `outbound_auth`,concat(`Numbers`.`CID`,' <',`Numbers`.`Number`,'>') AS `callerid`,if(`Numbers`.`Protocol` = 'TLS','sdes','no') AS `media_encryption`,`Numbers`.`PickupGroup` AS `named_pickup_group`,2 AS `device_state_busy_at`,concat('vRecord=',if(find_in_set('Record',`Numbers`.`Flags`) > 0,'yes','no'),';','vRussia=',if(find_in_set('Russia',`Numbers`.`Flags`) > 0,'yes','no'),';','vAbroad=',if(find_in_set('Abroad',`Numbers`.`Flags`) > 0,'yes','no'),';','vVoicemail=',if(find_in_set('Voicemail',`Numbers`.`Flags`) > 0,'yes','no'),';','vFilterCID=no') AS `set_var`,`Numbers`.`NumberID` AS `callerid_tag`,'username,auth_username,ip' AS `identify_by` from `Numbers`;

/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

Здесь немного поясню. Решил немного усложнить задачу и дополнить условием - одновременно должно существовать два типа нумерации - "глобальная" и "местная":

  • Глобальная, возьмем что это будет 5ти значные номера. Будем выдавать их в центре и мобильным сотрудникам, которые перемещаются между филиалами. Данные номера уникальны, и не могут повторяться.

  • Местная, пусть это будет 3х знак, некоторый универсальный номерной план внутри филиала, номера могут повторяться от филиала к филиалу, что позволяет нам сделать стандарт, типа, у директора филиала номер всегда 101, главного бухгалтера 102, зав склада по прозвищу "Вжик" 007 и т.д.

Ок, теперь заполним таблицу Numbers некоторыми тестовыми данными и взглянем на наши вьюшки.

Таблица Numbers:

Отлично, "входная точка для админа" выглядит вполне хорошо, минимум полей, максимум наглядности.

ps_endpoints

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

ps_auth

ps_aors

Так, выглядит замечательно. Более того, править будем только таблицу Numbers, поэтому самое время выдохнуть с облегчением.

Думаю многие уже догадались, где скрывается домен. Для тех кто пока еще не понял - посмотрите на столбцы id. Для 5ти знака, там записан просто номер - потому что мы не связываем с каким либо определённым доменом эту нумерацию. А вот для 3х знака мы прописываем после номера через @ домен. Дело в том, что астериск сначала ищет номер и авторизацию с доменом, и если такой в таблице нет - без.

Хочу акцентировать внимание на еще одном моменте. Он не очевиден, и многие спотыкаются и бросают эту тему. Имя AOR для endpointa ДОЛЖНО содержать домен, т.е. быть в формате номер@домен, если номер указан с доменом. Дело в том, что если в номере (id) присутствует домен, то астериск будет искать AOR с доменом. Это захардкорено в код. Еще раз - дать произвольное имя для AOR НЕ получится, это просто не будет работать. Нюанс, однако.

И так, почти все готово, давайте быстро пробежим по настройке связки астериска с базой.

pjsip.conf

Тут давайте немного тормознем, и посмотрим на опции.

disable multi domain

Это опция интересна тем, что по умолчанию она no. Серьезно, чаще всего инсталляции идут без поддержки домена. Но значение по-умолчанию дает астериску команду проверять домены. Если у Вас нет доменов, не поленитесь, поставьте здесь yes. Это сэкономит по 2 обращения на каждую авторизацию и каждый инвайт. База скажет вам спасибо.

endpoint identifier order

Эта опция интересна тем, что правильная ее настройка, экономит нам такты процессора. Значение по умолчанию у ней = ip,username. В такой конфигурации, астериск заточен на работу с транками без авторизации, т.е. когда авторизация идет по IP адресу. Если же у нас высоконагруженный сервер регистрации, мы можем вперед выставить username, что позволит астериску находить нужный endpoint на пару десятков тактов процессора быстрее.

по опциям все, погнали дальше по конфигам.

res config mysql.conf

Тут все очень просто, так что просто листинг - тестовая база стоит локально в этом примере:

extconfig.conf

Тут тоже ничего сложного, просто связываем идентификаторы с таблицами в базе.

ps_contacts можно не связывать, она прекрасно будет себя чувствовать и в astdb.

Spoiler

Наблюдательный читать обратил внимание на некоторый идентификатор CDR. Это один из моих экспериментов, по написанию модуля для выгрузки статистики звонков через механизмы реалтайм. Если заинтересовало, велком на GitHub

sorcery.conf

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

Перезапускаем астериск.

Собственно, где результат?

Если все сделано правильно, то у нас должны отобразиться такие endpoint в консоли астериска:

Не буду приводить всю портянку, суть видна на скрине и так. "Глобальные номера" у нас как и обычно без домена. А вот "местная нумерация" вся идет с доменном.

Обращаю внимание, что в диалплане нужно учитывать домен. Т.е., на 5ти знаки:

Dial(PJSIP/19960)

а вот на 3х знаки

Dial(PJSIP/123@test3)

Диалплан получается немного хитрый, но тем не менее, реализовать его не сложно.

Как зарегистрировать абонента?

Microsip, настройки будут выглядеть вот так:

Теперь каждый филиал может иметь свой номер 101 для директора. Разве это не прекрасно?

PS: Диалплан

Первый же вопрос в комментариях - а как звонить между филиалами.

Скажу избитую фразу - диалплан, это кусок творчества, сделать его можно по разному и так как этого требует задача. Поэтому ниже, это лишь пример.

Делаем таблицу domains:

domain (VARCHAR) | code (VARCHAR)

Заполняем, к примеру, вот так:

test2 | 01

test3 | 02

Код филиала можно взять конечно любой - даже 1 знак. Определяемся с выходом на филиалы. Например, у меня, выход на "город" через 9, а на филиалы я возьму 8. Вы берете то, что удобно Вам. Хоть * (звездочка).

Связываем в extconfig.conf таблицу domains с идентификатором domains. Пишем диалплан:

exten => _8XXXXX,1,NoOp(Call from ${CALLERID(all)} to filial ${EXTEN:1:2})
same => n, Set(domain=${REALTIME_FIELD(domains,code,${EXTEN:1:2},domain)})
same => n, GotoIF($[ "${domain}" = "" ]?GotoError) ; Обрабатываем не верный код филиала
same => n, Dial(PJSIP/${EXTEN:3}@${domain})
same => n, Hangip

Итого, набираем на телефоне, 801123 и попадаем в филиал test2 на номер 123.

Для того, чтобы было удобнее администрировать, можно добавить еще ограничение ключа:

Numbers.domain - FOREIGN KEY -> domains.domain

Это позволит не потерять домены при редактировании.