image
IP ATC Asterisk — это мощный комбайн в области IP-телефонии. А web-интерфейс FreePBX, созданный для Asterisk, значительно упрощает настройку и снижает порог вхождения в систему.
Если вы можете придумать какую-либо задачу, связанную с IP-телефонией, то почти наверняка это можно реализовать в Asterisk. Но будьте уверены, что от вас потребуется упорство и выдержка.

Перед нами встала задача настроить e-mail уведомления о пропущенных вызовах. Точнее говоря, оповещать через e-mail о тех случаях, когда входящий вызов перешёл в очередь, но никто (из агентов) так и не ответил на этот входящий вызов.

На удивление мы не обнаружили штатных средств для решения этой задачи во FreePBX. О том, как мы решили эту задачу, расскажу под катом.

Предисловие

Перед решением задачи «в лоб» мы конечно поискали информацию в интернете, но решения под ключ не нашли (возможно плохо искали, но что поделаешь… ).

Навыков работы непосредственно в Asterisk не так много, как хотелось бы, поэтому решение, предлагаемое здесь, не было до конца осмыслено и было отброшено.

Понравилось решение, предложенное здесь, хоть оно и не заработало. Отсюда подчеркнули, что работать в Asterisk нужно в контексте очередей [ext-queues]. И так как мы работаем во Freepbx, то работать нужно в файле конфигурации «extensions_override_freepbx.conf». Обратили внимание на то, что «ловить пропущенные вызовы» удобно перед событием hangupcall (окончание вызова).
Прочитав обсуждение здесь, появилась идея о том, что нужно фильтровать в CDR переменную «Disposition» по всем агентам в очереди. А после прочтения этой информации сформировались вполне конкретные шаги по решению поставленной задачи.

Что у нас есть:

Есть FreePBX 13.0.197, который использует Asterisk 13.12.1. Версия ОС SHMZ release 6.6 (Final). Дистрибутив базируется на CentOS.

В Asterisk настроен IVR (голосовое меню) раскидывающий входящие вызовы на разные Queues (очереди). Каждой очереди назначены Agents (агенты), т. е. операторы.

Теория

Что происходит в Asterisk

Когда на Asterisk поступает входящий вызов, этот вызов попадает на IVR. Звонящий делает выбор, нажав определенную цифру на телефоне, и попадает в определенную очередь. После этого всем свободным агентам очереди одновременно поступает звонок.

Для того чтобы лучше понять, что происходит в этот момент и что происходит дальше, обратимся к Report CDR (Рис.1).

image
Рис.1

Когда входящий вызов попал в очередь, у всех агентов значение переменной «Disposition» стало равным «NO ANSWER», если агенты в этот момент были не заняты. Переменная «Disposition» могла принять и другие значения (см. https://asterisk-pbx.ru/wiki/asterisk/cf/cdr), кроме значения «ANSWERED». А в тот момент, когда один из агентов отвечает на входящий вызов, значение переменной «Disposition» этого агента становится равной «ANSWERED».
Из Report CDR можно заметить, что когда вызов перешел в очередь (в колонке App значение становится равным «Queue»), то все события фигурируют с одинаковым «uniqueid» (колонка System).

Коротко о CDR

Важно понимать что такое CDR, и в какой именно момент в CDR заносятся данные, которые мы наблюдаем в Report CDR. CDR, относительно операционной системы — это база данных, в которую Asterisk записывает детализированный отчет вызовов (см. https://asterisk-pbx.ru/wiki/asterisk/cf/cdr). В нашем случае это база под именем asteriskcdrdb, которая находится в mysql. Опытным путем мы установили, что данные о вызове с определенным «uniqueid» заносятся в asteriskcdrdb не сразу после возникновения какого-либо события, а после события hangupcall (окончание вызова).

Принцип работы созданного решения

Так как у нас познаний в bash больше, чем познаний в Asterisk, то основная идея получилась следующей. Перед событием hangupcall вызвать bash-скрипт. В этот скрипт передать 3 параметра. Первый параметр «uniqueid», для фильтрации данных, получаемых из CDR. Второй параметр «CALLERID(num)» (номер звонившего), чтобы знать кому перезвонить. Третий параметр «NODEST» (номер очереди), в которую поступил звонок, для того, чтобы знать по какому вопросу был звонок, и кому отправить e-mail уведомление о пропущенном вызове.
Bash-скрипт должен подключиться к базе asteriskcdrdb в mysql и взять все значения переменной «Disposition» с определенным «uniqueid». Из полученных данных нужно исключить значения: «NO ANSWER», «BUSY», «FAILED», «UNKNOWN». В результате останутся либо «ANSWERED» — на входящий вызов ответили, либо вообще ни чего — пропущенный вызов.

Далее, если вызов оказался пропущенным, то скрипт должен отправить e-mail уведомление.
Забегая вперед отмечу важный момент. Asterisk выполняет команды последовательно, дожидаясь их выполнения (что в общем-то логично). А вызывать bash-скрипт мы будем до того, как выполнится команда hangupcall. Таким образом в момент непосредственного выполнения скрипта, в CDR еще не будет внесена информация об искомом нами «uniqueid». Для решения этой проблемы bash-скрипт мы будем вызывать с параметром «&», чтобы Asterisk сразу перешел к выполнению следующего шага, т. е. hangupcall. А внутри bash-скрипта, в самом начале, мы установим небольшую задержку по времени, чтобы дать время для Asterisk внести данные с интересующим нас «uniqueid» в CDR.

Практика

Перед тем как перейти к настройке Asterisk и созданию bash-скрипта, нужно настроить отправку e-mail уведомлений. Для этого мы будем использовать утилиту postfix.

Настройка postfix

У нас есть почтовый домен «lucky.ru», расположенный в Яндексе. Мы настроим postfix в режим smtp-клиента и будем отправлять письма с аккаунта asterisk@lucky.ru.
За основу взято решение отсюда: https://www.dmosk.ru/miniinstruktions.php?mini=postfix-over-yandex.

Сначала установим/обновим/проверим наличие пакетов:

yum install postfix
yum install mailx
yum install cyrus-sasl cyrus-sasl-lib cyrus-sasl-plain

Не будем затирать основной файл конфигурации postfix «/etc/postfix/main.cf», а создадим его резервную копию:

cp /etc/postfix/main.cf /etc/postfix/main.cf.sav

Редактируем файл «/etc/postfix/main.cf» и приводим его к следующему виду:

nano /etc/postfix/main.cf
#####################
relayhost =
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/private/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_type = cyrus
smtp_sasl_mechanism_filter = login
smtp_sender_dependent_authentication = yes
sender_dependent_relayhost_maps = hash:/etc/postfix/private/sender_relay
smtp_generic_maps = hash:/etc/postfix/generic
smtp_tls_CAfile = /etc/postfix/ca.pem
smtp_use_tls = yes
smtputf8_autodetect_classes = all
#####################

Не каждую строку в «/etc/postfix/main.cf» можно комментировать. Комментарии в некоторых строках не определяются парсером и передаются в обработку, а это приводит к ошибкам. Лучше отказаться от комментариев внутри этого файла. Можете поэкспериментировать с этим запустив в соседнем окне «tail -f /var/log/messages».

Отмечу строку «smtputf8_autodetect_classes = all». Эта запись включает utf-8 по умолчанию, что позволяет использовать кириллицу и в теле письма, и в теме письма без дополнительных манипуляций (См. http://www.postfix.org/SMTPUTF8_README.html).

Создадим каталог для файлов конфигураций:

mkdir /etc/postfix/private

Редактируем файл «/etc/postfix/private/sender_relay». В нем нужно указать на какой smtp-сервер нужно ссылаться при использовании нашего почтового домена:

nano /etc/postfix/private/sender_relay
#####################
@lucky.ru smtp.yandex.ru
#####################

Редактируем файл «/etc/postfix/private/sasl_passwd». В нем мы укажем e-mail адрес, который мы будем использовать для отправки писем, а так же логин и пароль от этой учетной записи (логин и пароль указываем через двоеточие):

nano /etc/postfix/private/sasl_passwd
#####################
asterisk@lucky.ru asterisk@lucky.ru:password_asterisk
#####################

Редактируем файл «/etc/postfix/generic». В нем мы пропишем правила подмены исходящего адреса (см. https://wiki.merionet.ru/ip-telephoniya/30/postfix-nastrojka-otpravki-pochty-v-asterisk/):

nano /etc/postfix/generic
#####################
root asterisk@lucky.ru
root@localhost asterisk@lucky.ru
root@localhost.localdomain asterisk@lucky.ru
root@freepbx asterisk@lucky.ru
root@freepbx.localdomain asterisk@lucky.ru
root@asterisk asterisk@lucky.ru
root@asterisk.localdomain asterisk@lucky.ru
asterisk asterisk@lucky.ru
asterisk@localhost asterisk@lucky.ru
asterisk@localhost.localdomain asterisk@lucky.ru
asterisk@freepbx asterisk@lucky.ru
asterisk@freepbx.localdomain asterisk@lucky.ru
asterisk@asterisk asterisk@lucky.ru
asterisk@asterisk.localdomain asterisk@lucky.ru
root@localdomain.localdomain asterisk@lucky.ru
#####################

Изначальный исходящий адрес зависит от содержимого «/etc/hosts» и «/etc/hostname», а также от имени пользователя, который будет отправлять письмо. Т. е. не смотря на то, что мы используем smtp-клиент и отправляем письма от asterisk@lucky.ru, все равно в адрес отправителя postfix изначально подставит «что-то своё» и это нужно исправить правилами из этого файла конфигурации.

Приведу содержимое своего файла «/etc/hosts»:

cat /etc/hosts
#####################
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 asterisk.localdomain
127.0.0.1 localhost.localdomain localhost
::1 asterisk localhost localhost6
#####################

Важно, чтобы сервер имел какой-либо домен (значение после точки), потому что утилита mail «ищет» имя домена в «/etc/hosts» и если «не находит» его сразу, то продолжит это делать в течение еще нескольких минут и только потом отправит письмо. Т. е. если домен не прописан, то письмо будет уходить с задержкой в несколько минут.

Приведу содержимое своего файла «/etc/hostname»:

cat /etc/hostname
#####################
asterisk
#####################

Далее необходимо перевести созданные файлы конфигурации в индексированные базы данных, для этого выполним следующую команду:

postmap /etc/postfix/generic && postmap /etc/postfix/private/{sasl_passwd,sender_relay}

Далее нам необходимо скачать и разместить на сервере сертификат smtp.yandex.ru, для этого выполним следующую команду:

openssl s_client -starttls smtp -crlf -connect smtp.yandex.ru:25 > /etc/postfix/ca.pem

Но после того, как на экран выйдет техническая информация команда будет «продолжать висеть». Нажмите Ctrl+C чтобы прервать её.

Теперь вручную удалим из получившегося файла весь мусор и оставим только сертификат. Должно получиться нечто подобное:

nano /etc/postfix/ca.pem
#####################
-----BEGIN CERTIFICATE-----
MIIGazCCBVOgAwIBAgIQcUU9mJXW4OUs5Gf0JfLtsjANBgkqhkiG9w0BAQsFADBf
...
nRG0DfdqYIuPGApFORYe
-----END CERTIFICATE-----
#####################

И наконец перезапустим postfix:

service postfix restart

Отправляем тестовое письмо:

echo "Это тело письма" | mail -s "Это тема" admin@lucky.ru

admin@lucky.ru — адрес назначения

На этом настройка posfix закончена.

Пишем bash-скрипт

Создаем директорию для хранения bash-скрипта (тут кому где больше нравится):

mkdir /home/asterisk/scripts

Создаем файл bash-скрипта:

touch /home/asterisk/scripts/noanswer.sh

Выдаем файлу скрипта права на выполнение:

chmod +x /home/asterisk/scripts/noanswer.sh

Если есть сомнения в правах на файл, то на время отладки можно дать полный доступ к файлу. Но это «не безопасно».

chmod 777 /home/asterisk/scripts/noanswer.sh

Текст bash-скрипта:

nano /home/asterisk/scripts/noanswer.sh
#####################
#!/bin/bash

sleep 7

res_sql="SELECT disposition FROM cdr WHERE uniqueid = '$1'"

answer=`mysql -u freepbxuser -pPassword_freepbxuser -D asteriskcdrdb -B -N -e "$res_sql" | grep -E -v "NO ANSWER|BUSY|FAILED|UNKNOWN" | head -n 1`

error_kod=0
if [ "$answer" != "ANSWERED" ]
then

 case $3 in
 68800)
 address="big_boss@lucky.ru"
 subject="по важному вопросу"
 ;;
 63100)
 address="debian@lucky.ru"
 subject="по вопросам linux debian"
 ;;
 63200)
 address="windows@lucky.ru"
 subject="по вопросам windows"
 ;;
 63300)
 address="freebsd@lucky.ru"
 subject="по вопросам freebsd"
 ;;
 63400)
 address="ubuntu@lucky.ru"
 subject="по вопросам linux ubuntu"
 ;;
 63500)
 address="centos@lucky.ru"
 subject="по вопросам linux centos"
 ;;
 *)
 address="admin@lucky.ru"
 error_kod=1
 ;;
 esac

 case $error_kod in
 0)
 echo "Пропущен вызов от абонента $2, звонившего $subject." | mail -s "Пропущен вызов от $2" $address
 echo "Пропущен вызов для $address от абонента $2, звонившего $subject. uid=$1" | mail -s "Пропущен вызов от $2" admin@lucky.ru
 ;;
 1)
 echo "Пропущен вызов от $2. Очередь неизвестна. uid=$1" | mail -s "Пропущен вызов от $2" admin@lucky.ru
 ;;
 esac

fi
#####################

Краткий разбор скрипта:
«sleep 7»:

Это та самая задержка по времени, о которой я писал ранее. У нас установлена задержка на 7 сек. Хотя, думаю, и одной секунды вполне хватит.

«res_sql="SELECT disposition FROM cdr WHERE uniqueid = '$1'"»:

Запрос в mysql мы вынесли в отдельную переменную для удобства.

Далее мы делаем запрос в mysql и фильтруем полученный вывод. Удаляем все варианты кроме «ANSWERED», если такой вообще есть. Если же значений «ANSWERED» несколько, то нужно оставить только одно. В конце в переменную «answer» мы получим либо «ANSWERED» либо «».
Если значение переменной «answer» не равно «ANSWERED», то это пропущенный вызов. В зависимости от номера очереди, с помощью оператора case мы зададим адрес, кому именно необходимо отправить e-mail уведомление, и что в этом сообщении написать (изменяемая часть сообщения).

Далее рассмотрен вариант, когда очередь задана в Asterisk, но не описана в скрипте. В этом случае admin@lucky.ru получит письмо, о том, что очередь не известна скрипту.

Если же очередь описана, то будет отправлено письмо по назначению и дублирующее письмо на admin@lucky.ru с указанием «uniqueid», для того, чтобы можно было отследить события по этому звонку, в случае необходимости.

На этом скрипт заканчивается.

Отмечу, что для подключения к mysql мы использовали логин и пароль, которые заранее узнали. Во FreePBX для того, чтобы узнать логин пользователя Asterisk в mysql выполните следующую команду:

cat /etc/amportal.conf | grep AMPDBUSER

А для того, чтобы узнать пароль пользователя Asterisk в mysql выполните следующую команду:

cat /etc/amportal.conf | grep AMPDBPASS

Настройка Asterisk

Мы используем FreePBX. Во FreePBX есть разные типы конфигурационных файлов (см. https://asterisk-pbx.ru/wiki/freepbx/files), некоторые из них FreePBX перезаписывает при перезагрузке, а некоторые не перезаписывает (их называют custom), так как они специально предназначены для пользователя.

Мы будем работать с файлом конфигурации «extensions_override_freepbx.conf», так как он относится к типу custom.

Для начала убедимся, что в файле «/etc/asterisk/extensions.conf» подключен файл «extensions_override_freepbx.conf». Для этого выполним следующую команду:

cat /etc/asterisk/extensions.conf | grep extensions_override_freepbx.conf
#####################
#include extensions_override_freepbx.conf
#####################

Редактируем файл «/etc/asterisk/extensions_override_freepbx.conf» и приведем его к следующему виду:

nano /etc/asterisk/extensions_override_freepbx.conf
#####################
[ext-queues]

exten => h,1,System(/home/asterisk/scripts/noanswer.sh ${CDR(uniqueid)} ${CALLERID(num)} ${NODEST} &)
exten => h,2,Macro(hangupcall,)
#####################

Как я и писал ранее символ «&» в конце обязателен. Так как мы будем работать в bash-скрипте с данными CDR непосредственно из базы mysql, а эти данные заносятся в mysql только после выполнения «exten => h,2,Macro(hangupcall,)», то необходимо не ждать окончания отработки bash-скрипта, а перейти к выполнению следующего шага в Asterisk. А сам bash-скрипт должен содержать задержку по времени, перед выполнением основной своей части.

Для того, чтобы изменения в конфигурационном файле «/etc/asterisk/extensions_override_freepbx.conf» вступили в силу необходимо перезагрузить ядро Asterisk следующей командой:

/usr/sbin/asterisk -rx "core restart now"

Это нужно сделать после того как bash-скрипт будет создан.

Заключение

Наверное, это 1001-й способ «отлова пропущенных вызовов» в Asterisk. Поделитесь в комментариях как эту задачу решаете вы. И что, по вашему мнению, можно доработать/переделать/оптимизировать. Будем признательны за конструктивные идеи.

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


  1. arheops
    15.08.2019 21:47

    Не делайте так, используйте queue_log.

    Также астериск вообще говоря не гарантирует запись CDR в таблицу до запуска h-exten. Вы вроде как решили это через sleep 7, НО астериск не гарантирует и за 7 секунд вообще говоря.
    А еше на нагруженной системе скрипты с sleep 7 будут убиваться случайно.

    Корректным решением был бы скрипт, который мониторит в mysql queue_logs и по записи ABANDON шлет сообщение.


    1. volos4
      16.08.2019 14:56

      а еще queue_log можно писать в mysql и там настроить триггер на ABANDON


      1. arheops
        16.08.2019 16:20

        Правильно, писать в mysql и потом писать тригер.
        Тригеры mysql както странно работают на внешние скрипты. Я просто проверяю табличку.


        1. vladimir1211 Автор
          17.08.2019 18:10

          А зачем писать в mysql еще что-то, если имеющихся в нём данных и так достаточно для решения задачи?


  1. lryzhik
    16.08.2019 00:48

    простите, а почему select | grep, а не добавить в where условие? думаю, что при наличии индекса на поле, будет быстрее. проверяли ли этот вариант?


    1. vladimir1211 Автор
      16.08.2019 05:07

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

      res_sql="SELECT disposition FROM cdr WHERE uniqueid = '$1' AND disposition = 'ANSWERED'"
      answer=`mysql -u freepbxuser -pPassword_freepbxuser -D asteriskcdrdb -B -N -e "$res_sql" | head -n 1`


  1. awsswa59
    16.08.2019 08:54

    Не пользуйтесь postfix если вам надо просто отправлять почту… есть ssmtp
    конфигурация из 5 строк.
    И используйте mutt при отправке письма


  1. awsswa59
    16.08.2019 08:57

    Отлавливание пропущенных делается проще. Добавляете в Dial вызов gosub при ответе вызова.
    В gosub ставите метку что вызов отвечен.
    По hangupcause проверяете наличие метки.
    Отправляете почту при отсутствии.

    Очередь, не очередь, группа, номер… везде работает.
    И не надо в mysql лезь и bash изобретать


  1. Rsa97
    16.08.2019 09:16

    А почему не сделать проще?
    У команды QUEUE есть ключ 'c', при котором диалплан продолжается после того, как вызывающий положил трубку, и переменная ABANDONED, которая выставляется в TRUE, если никто из агентов не ответил на вызов.
    same => n,Queue(callcenter,CcRt,,,,,,callcenter-queue-connected)
    same => n,GotoIf($["${ABANDONED}"="TRUE"]?send-email)


  1. NikolaBY
    16.08.2019 10:04

    Очень сложное решение и непрактичное решение. Понятное дело, что нехватку практического опыта надо компенсировать и это похвально, решение все же работает :) Rsa97 выше предложил очень хорошее решение :) на практике мы что-то подобное используем уже довольно долго, и поток из тысяч звонков в день показывают его надежность и эффективность.


  1. Ovoshlook
    19.08.2019 05:58

    Откажитесь от вызова System. Любой внешний вызов через System/Exec и тд очень сильно грузит систему, что особенно заметно при хорошей нагрузке.
    Если нужно выполнение внешнего скрипта в любой задаче — пишите нормальный демон скрипта и кидайте ему данные через curl, например, или слушайте астериск через AMI/ARI.