К основному содержанию

Кейс

Телефонная книга из NetBox в Asterisk: имя на аппарате и карточка контакта в Telegram

Как связать источник истины по контактам (NetBox) с АТС на Asterisk, которая не умеет ходить в REST из диалплана: реверс-поиск номер→имя через локальный кэш astdb, нормализация 8xx/+7xxx/7xxx к последним 10 цифрам и обогащение CDR-уведомлений в Telegram кликабельными ссылками на объекты NetBox.

3300 контактовиз NetBox отдаются как имя звонящего на аппарат
Как контакт проходит через телефонию

Схема читается сверху вниз. В критичном пути звонка сетевого запроса нет: имя берётся из локальной базы.

01До звонка и во время звонка

NetBoxИсточник истины: контакты, телефоны, тенанты и сайты
Синхронизация раз в часREST-выгрузка контактов в локальную базу astdb
AsteriskCaller ID читается из astdb, диалплан и запись разговора

На аппарат уходит имя. Сетевого запроса в критичном пути звонка нет — станция не ждёт чужую систему

02После звонка

Отдельный поиск в NetBoxСвежие связи контакта: contact, tenant, site
Сборка карточкиCDR, запись разговора и ссылки на связанные записи
TelegramКто звонил, запись и ссылки в NetBox

Медленная часть вынесена за пределы звонка: карточка собирается тогда, когда ждать уже некому

У нас есть источник истины по инфраструктуре и контрагентам — NetBox: там тенанты, сайты, устройства и контакты с телефонами. И есть рабочая АТС на Asterisk. Задача звучала просто: когда звонит контакт из NetBox, показывать на аппарате его имя, а не голый номер. А заодно — дописать имя и связи контакта в те CDR-уведомления, что АТС и так шлёт в Telegram.

«Просто» закончилось на первом же вопросе: как именно АТС узнаёт имя по номеру.

Ограничение, которое определило всю архитектуру

Очевидный первый подход — дёрнуть REST NetBox прямо из диалплана в момент звонка. На нашей АТС он не работает: в её диалплане нет функций для HTTP-запроса и разбора JSON. Живой запрос из плана набора отпадает.

Значит, инвертируем: не АТС ходит в NetBox на каждом звонке, а мы периодически складываем контакты в локальный кэш, из которого диалплан читает мгновенно. У Asterisk для этого есть встроенное key-value хранилище — astdb. Раз в час скрипт на стороне АТС читает контакты NetBox (read-only токен), нормализует телефоны и раскладывает в семью cidname: ключ — номер, значение — имя.

Этот выбор — не «как правильно», а «как возможно в этих условиях». И он же оказался удачным: реверс-поиск на каждом звонке — это чтение из локальной базы за доли миллисекунды, без сетевой зависимости в критичном пути маршрутизации вызова.

Реверс-поиск: номер → имя

Контакт может быть записан как +7 900 000-00-00, 8 (900) 000-00-00 или 79000000000 — форматов в реальной базе столько же, сколько людей их заполняло. Чтобы поиск не зависел от формата, и при синхронизации, и при чтении номер сводится к последним 10 цифрам с фиксированным префиксом:

key = "7" + last10(digits(number))

8xxxxxxxxxx, +7xxxxxxxxxx и 7xxxxxxxxxx дают один и тот же ключ. В диалплане подстановка имени — одна строка, срабатывающая только если в кэше что-то нашлось (и не затирающая имя, если нет):

same => n,ExecIf($["${DB(cidname/7${CALLERID(num):-10})}"!=""]?Set(CALLERID(name)=${DB(cidname/7${CALLERID(num):-10})}))

Отдельная тонкость — несколько номеров в одном поле контакта (мобильный и городской через запятую). Наивный разбор «взять все цифры подряд» склеивал бы их в одну строку из 20+ цифр и выкидывал контакт целиком. Поэтому поле бьётся по явным разделителям (запятая, точка с запятой, перевод строки), и каждый валидный номер маппится на одно имя. Пробелы и слэши внутри одного номера при этом не трогаем — они часть формата, а не разделители списка.

Карточка контакта в Telegram

АТС и раньше отправляла в Telegram уведомление по каждому звонку (CDR) — статус, номера, запись разговора. Теперь в него добавились две вещи.

Первая — имя. На входящем это имя звонящего (то же, что ушло на аппарат), на исходящем — имя адресата: кому звонил оператор. Резолв — по тем же последним 10 цифрам, так что формат набранного номера роли не играет.

Вторая — связи контакта. Это уже не на аппарат (там только имя и помещается), а именно в сообщение: отдельная секция со ссылками прямо в NetBox.

🟢 ОТВЕЧЕНО

Кто звонил: 102
Куда звонил: Иван Петров  89000000000

NetBox
Contact: Иван Петров  tenancy.contact:123
Tenant:  acme         tenancy.tenant:7
Site:    msk-1        dcim.site:4

Значения моноширинные (удобно скопировать), а <object_type>:<id> — кликабельная ссылка, открывающая объект в NetBox. Здесь имеет смысл живой запрос: уведомление формируется после звонка, асинхронно, на поток вызовов не влияет — поэтому связи берутся свежими из NetBox по номеру, а не кэшируются. Таймаут ограничен, недоступность NetBox просто убирает секцию, но само уведомление уходит.

Грабли, которые стоили времени

Что с одновременными звонками

Уведомления не выстроены в общую очередь: на каждый звершённый звонок диалплан порождает независимый процесс. Это не «узкое место», а веерная параллельность, и она безопасна ровно потому, что нет общего изменяемого состояния: чтения из astdb атомарны, файл записи у каждого звонка свой, лог только дописывается. Десяток одновременных обращений отрабатывает за секунду с небольшим, нагрузка на узел — околонулевая. Единственный сценарий накопления процессов — длительная недоступность Telegram, и он лечится разумным лимитом ретраев, а не переделкой схемы.


Урок здесь не про Asterisk и не про NetBox по отдельности, а про стык легаси-систем и современных источников истины. Такая АТС не станет ходить в современный REST — но ей это и не нужно: правильная граница проходит не «пусть железо научится новому протоколу», а «пусть источник истины сам доставит данные туда, где их прочитают дёшево». Реверс-поиск по последним 10 цифрам, локальный кэш и асинхронное обогащение уведомления — три маленьких решения, каждое из которых обходит конкретное ограничение, а не борется с ним. В сумме контакт из NetBox теперь виден и на трубке телефониста, и в Telegram — одним кликом до карточки в источнике истины.

Все кейсы