# Глава 15. Интеграция реляционной базы данных

> Нет ничего более раздражающего, чем хороший пример
>
> _--Марк Твен_

В этой главе мы рассмотрим интеграцию некоторых особенностей и функций Asterisk в базу данных. Существует несколько баз данных, доступных для Linux, и Asterisk поддерживает наиболее популярные из них через свой ODBC-коннектор. Хотя в этой главе будут продемонстрированы примеры использования ODBC-коннектора с базой данных MySQL, вы также обнаружите, что большинство концепций будет применяться к любой базе данных, поддерживаемой unixODBC.

Интеграция Asterisk с базами данных является одним из фундаментальных аспектов построения большой кластерной или распределенной системы. Мощь базы данных позволит вам использовать динамически изменяющиеся данные в ваших диалпланах для таких задач, как обмен информацией в массиве систем Asterisk или интеграция с веб-службами. Наша любимая функция диалплана, которую мы рассмотрим позже в этой главе - это `func_odbc`. Мы также рассмотрим архитектуру Asterisk Realtime Architecture (ARA), записи сведений о вызовах (CDR) и сведения о регистрации из любых очередей ACD, которые у вас могут быть.

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

## Ваш выбор базы данных
В [Главе 3](glava-03.md) мы установили и настроили MySQL плюс ODBC-коннектор к нему и использовали таблицы, которые предоставляет Asterisk, чтобы позволить различным параметрам конфигурации храниться в базе данных.

Мы выбрали MySQL в первую очередь потому, что это самый популярный движок баз данных с открытым исходным кодом, и вместо того, чтобы прыгать вокруг, дублируя тривиальные команды на разных движках, мы оставили реализацию других типов баз данных для набора навыков читателю. Если вы хотите использовать другую базу данных, такую как MariaDB, PostGreSQL, Microsoft SQL, или фактически десяток (возможно, сотни) других баз данных, поддерживаемых unixODBC, вполне вероятно, что Asterisk будет работать и с ней.

Asterisk также предлагает собственные коннекторы для нескольких баз данных; однако ODBC работает так хорошо, что мы никогда не находили очевидной причины делать что-то иначе. Мы собираемся как рекомендовать использовать ODBC, так и сосредоточиться исключительно на нем. Если вы предпочитаете что-то другое, эта глава все равно должна предоставить вам основные принципы, а также некоторые рабочие примеры, и оттуда вы, конечно, можете перейти к другим методологиям.

Обратите внимание, что независимо от выбранной базы данных, эта книга не может научить вас базам данных. Мы постарались, насколько это возможно, привести примеры, которые не требуют слишком большого опыта в администрировании баз данных (DBA), но простой факт заключается в том, что базовые навыки DBA являются необходимым условием для полного использования возможностей любой базы данных, в том числе любой, которую вы можете интегрировать с вашей системой Asterisk. В наши дни навыки работы с базами данных необходимы практически для всех дисциплин системного администрирования, поэтому мы сочли целесообразным предположить, по крайней мере, базовый уровень знакомства с концепциями баз данных.

## Управлением базами данных

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

Некоторые из тех, которые мы использовали, включают:

* [phpMyAdmin](http://www.phpmyadmin.net/)
* [MySQL Workbench](http://wb.mysql.com/)
* [Navicat (коммерческая)](http://www.navicat.com/)

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

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

### Устранение неисправностей базы данных

При работе с подключениями к базе данных ODBC и Asterisk важно помнить, что подключение ODBC абстрагирует часть информации, передаваемой между Asterisk и базой данных. В тех случаях, когда все работает не так, как ожидалось, вам может потребоваться включить ведение лога для базы данных, чтобы увидеть, что Asterisk отправляет в базу данных (например, какие инструкции `SELECT`, `INSERT` или `UPDATE` запускаются из Asterisk), что видит база данных и почему она может отклонять инструкции.

Например, одной из наиболее распространенных проблем, обнаруживаемых при интеграции базы данных ODBC, является неверно определенная таблица или отсутствующий столбец, который Asterisk ожидает. В то время как большие успехи были сделаны в виде адаптивных модулей, не все части Asterisk являются адаптивными. В случае хранения голосовой почты ODBC, возможно, вы пропустили столбец, например `flag`, который является новым, отсутствующим в версиях Asterisk до 11.<sup><a href="#sn1">1</a></sup> как уже отмечалось, чтобы понять, почему ваши данные не записываются в базу данных как положено, вы должны включить логирование на стороне базы данных, а затем определить, какой оператор выполняется и почему база данных отклоняет его.

### SQL-инъекция

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

В случае Asterisk вам нужно подумать о том, какие входные данные вы принимаете от пользователей (обычно то, что они могут отправить в диалплан) и работать над очисткой этого ввода, чтобы убедиться, что вы разрешаете только символы, которые действительны для вашего приложения. Например, типичный телефонный звонок допускает только цифры в качестве входных данных (и, возможно, символы `*` и `#`), поэтому нет никаких причин принимать любые другие символы. Имейте в виду, что протокол SIP позволяет больше, чем просто цифры в качестве части адреса, поэтому не думайте, что те, кто пытается скомпрометировать вашу систему, ограничатся только цифрами.

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

## Мощь диалплана с функцией func_odbc

Функциональный модуль диалплана `func_odbc` позволяет определять и использовать относительно простые функции в вашем диалплане, которые будут извлекать информацию из баз данных при обработке вызовов. Существует множество способов, которыми это может быть использовано, например, управление пользователями или разрешение совместного использования динамической информации в кластеризованном наборе машин Asterisk. Мы не будем утверждать, что это облегчит разработку и написание кода диалплана, но обещаем что это позволит вам добавить совершенно новый уровень мощности к вашим диалпланам, особенно если вам удобно работать с базами данных. Мы не знаем никого в сообществе Asterisk, кто бы не любил `func_odbc`.

Способ работы `func_odbc` заключается в том, что вы можете определять SQL-запросы, которым присваиваете имена функций. Файл _func_odbc.conf_ - это место, где вы указываете отношения между создаваемыми функциями и исполняемыми инструкциями SQL. Именованные функции, созданные в диалплане, используются для извлечения и обновления значений в базе данных.

Для того, чтобы у Вас было хорошее настроение для того, что следует, мы хотим чтобы вы представили себе Дагвуд сэндвич.<sup><a href="#sn2">2</a></sup>

Можете ли вы передать общий опыт такой вещи, показав кому-то фотографию помидора или размахивая ломтиком сыра? Едва. Именно с этой загадкой мы столкнулись, пытаясь привести полезные примеры того, почему `func_odbc` настолько мощен. Итак, мы решили собрать целый сэндвич для вас. Это довольно полный рот, но после нескольких укусов этого, арахисовое масло и желе никогда не будет тем же самым.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td><p align="center"><b>Отношения файлов конфигурации ODBC</b></p>
    <p>Чтобы Asterisk мог использовать ODBC из диалплана, все файлы должны быть выстроены в линию. Рисунок 5-1 пытается передать это визуально. Вы, вероятно, найдете эту диаграмму более полезной, как только проработаете примеры в следующих разделах.</p>
    </td>
  </tr>
</table>

![](pics/pic15-1.png)

_Рисунок 15-1. Отношения между func_odbc.conf, res_odbc.conf, /etc/odbc.ini (unixODBC) и подключением к базе данных_

## Мягкое введение в func_odbc

Прежде чем мы погрузимся в `func_odbc`, чувствуем, что немного истории не помешает.

Самое первое использование `func_odbc`, которое произошло когда его автор все еще находился в процессе его написания, также является хорошим введением в его использование. Клиент одного из авторов модуля отметил, что некоторые люди, звонившие в его коммутатор, придумали способ совершать бесплатные звонки через его систему. В то время как его конечным намерением было изменить свой диалплан для избежания этих проблем, ему нужно было внести в черный список определенные идентификаторы вызывающих абонентов, и база данных, которую он хотел использовать для этого, была базой данных Microsoft SQL Server.

За некоторыми исключениями, это фактический диалплан:
```
[span3pri]
exten => _50054XX,1,NoOp()
   same => n,Set(CDR(accountcode)=pricall)
   ; Does this callerID appear in the database?
   same => n,GotoIf($[${ODBC_ANIBLOCK(${CALLERID(number)})}]?busy)
   same => n(dial),Dial(DAHDI/G1/${EXTEN})
   same => n(busy),Busy(10) ; Да, вы в черном списке.
   same => n,Hangup
```
Этот диалплан, в двух словах, передает все вызовы в другую систему для целей маршрутизации, за исключением тех вызовов, чьи идентификаторы абонентов находятся в черном списке. Звонки, поступающие в эту систему, использовали блок из 100 семизначных DID. Вы заметите, что используется функция диалплана, которую вы не найдете ни в одной из функций, которые поставляются с Asterisk: `ODBC_ANIBLOCK()`. Вместо этого эта функция была определена в другом файле конфигурации _func_odbc.conf_:
```
[ANIBLOCK]
dsn=telesys
readsql=SELECT IF(COUNT(1)>0, 1, 0) FROM Aniblock WHERE NUMBER='${ARG1}'
```
Итак, ваша функция `ODBC_ANIBLOCK()`<sup><a href="#sn3">3</a></sup> подключается к источнику данных в _res_odbc.conf_ называемому `telesys` и выбирает количество записей, которые имеют номер, указанный аргументом, который является (ссылаясь на предыдущий диалплан) идентификатором вызывающего абонента. Номинально эта функция должна возвращать либо `1` (указывая, что идентификатор вызывающего абонента существует в таблице `Aniblock`), либо `0` (если это не так). Это значение также вычисляется непосредственно как `true` или `false`, что означает, что нам не нужно использовать выражение в нашем диалплане, для усложнения логику.

Вот в двух словах, то, что такое `func_odbc`: написание пользовательских функций диалплана, которые возвращают результат из базы данных. Далее, более подробный пример того, как можно использовать `func_odbc`.

## Веселимся с func_odbc: горячий стол

Ладно, вернемся к Дагвуд-сэндвичу, который мы обещали.

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

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

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

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

Давайте начнем с простых вещей, и создадим две новых учетных записи телефонов в нашей базе данных.

Во-первых, таблица конечных точек:
```
MySQL> INSERT INTO asterisk.ps_endpoints (id,transport,aors,auth,context,disallow,allow, \
direct_media,callerid)

VALUES
('HOTDESK_1','transport-tls','HOTDESK_1','HOTDESK_1','hotdesk','all','ulaw','no', \
'HOTDESK_1'),
('HOTDESK_2','transport-tls','HOTDESK_2','HOTDESK_2','hotdesk','all','ulaw','no', \
'HOTDESK_2');
```
И `auths`:
```
MySQL> INSERT INTO asterisk.ps_auths (id,auth_type,password,username)

VALUES
('HOTDESK_1','userpass','notsohot1','HOTDESK_1'),
('HOTDESK_2','userpass','notsohot2','HOTDESK_2');
```
Наконец `aors`:
```
MySQL> INSERT INTO asterisk.ps_aors
(id,max_contacts)

VALUES
('HOTDESK_1',1),
('HOTDESK_2',1);
```
Обратите внимание, что мы сказали этим двум конечным точкам войти в диалплан в контексте с именем `[hotdesk]`. Мы определим его в ближайшее время.

Это все для нашей конфигурации конечной точки. У нас есть несколько ломтиков хлеба, которые еще не стали бутербродом.

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

Подключитесь к консоли MySQL как `root`:
```
$ mysql -u root -p
```
Сначала нам нужна новая схема, чтобы разместить все это. Технически это возможно поместить в схему `asterisk`, но мы предпочитаем оставить её в покое, зарезервированную только для всех скриптов Alembik Asterisk, которые выполняются с ней во время обновлений.
```
MySQL> CREATE SCHEMA pbx;

MySQL> GRANT SELECT,INSERT,UPDATE,DELETE,EXECUTE,SHOW VIEW ON pbx.* TO 'asterisk'@'::1';

MySQL> GRANT SELECT,INSERT,UPDATE,DELETE,EXECUTE,SHOW VIEW ON pbx.* TO \
'asterisk'@'127.0.0.1';

MySQL> GRANT SELECT,INSERT,UPDATE,DELETE,EXECUTE,SHOW VIEW ON pbx.* TO \
'asterisk'@'localhost';

MySQL> GRANT SELECT,INSERT,UPDATE,DELETE,EXECUTE,SHOW VIEW ON pbx.* TO \
'asterisk'@'localhost.localdomain';

MySQL> FLUSH PRIVILEGES;
```
Затем создайте таблицу со следующим битом SQL:
```
CREATE TABLE pbx.ast_hotdesk
(
  id serial NOT NULL,
  extension text,
  first_name text,
  last_name text,
  cid_name text,
  cid_number varchar(10),
  pin int,
  status bool DEFAULT false,
  endpoint text,
  CONSTRAINT ast_hotdesk_id_pk PRIMARY KEY (id)
);
```
После этого заполните базу данных следующей информацией (некоторые значения, которые вы видите на самом деле, изменятся только после завершения работы диалплана, но мы включим их здесь в качестве примера).

В консоли MySQL выполните следующую команду:
```
MySQL> INSERT INTO pbx.ast_hotdesk
(extension, first_name, last_name, cid_name, cid_number, pin, status)

VALUES
('1101','Herb','Tarlek','WKRP','1101','110111',0)
('1102','Al','Bundy','Garys','1102','110222',0),
('1103','Willy','Loman','','1103','110333',0),
('1104','Jerry','Lundegaard','Gustafson','1104','110444',0),
('1105','Moira','Brown','Craterside','1105','110555',0);
```
Повторите эти команды, изменяя `VALUES` по мере необходимости, для всех записей, которые вы хотите иметь в базе данных.<sup><a href="#sn4">4</a></sup> После ввода примера данных можно просмотреть данные в таблице `ast_hotdesk`, выполнив простую инструкцию `SELECT` из консоли базы данных:
```
MySQL> SELECT * FROM pbx.ast_hotdesk;
```
Что может дать вам что-то вроде следующего вывода:
```
+--+---------+----------+----------+----------+----------+------+------+--------+
|id|extension|first_name|last_name |cid_name  |cid_number|pin   |status|endpoint|
+--+---------+----------+----------+----------+----------+------+------+--------+
| 1|1101     |Herb      |Tarlek    |WKRP      |1101      |110111|     0|NULL    |
| 2|1102     |Al        |Bundy     |Garys     |1102      |110222|     0|NULL    |
| 3|1103     |Willy     |Loman     |          |1103      |110333|     0|NULL    |
| 4|1104     |Jerry     |Lundegaard|Gustafson |1104      |110444|     0|NULL    |
| 5|1105     |Moira     |Brown     |Craterside|1105      |110555|     0|NULL    |
+--+---------+----------+----------+----------+----------+------+------+--------+
```
Теперь у нас есть приправы, так что давайте перейдем к нашему диалплану. Именно здесь произойдет волшебство.

Где-то в _extensions.conf_ мы собираемся создать контекст `[hotdesk]`. Для начала давайте определим расширение сопоставления с шаблоном, позволяющее пользователям входить в систему:
```
[hotdesk]
include => sets

exten => _*99110[1-5],1,Noop(Hotdesk login)
  same => n,Set(HotExten=${EXTEN:3}) ; strip off the leading *99
  same => n,Noop(Hotdesk Extension ${HotExten} is changing status) ; for the log
  same => n,Set(${HotExten}_STATUS=${HOTDESK_INFO(status,${HotExten})})
  same => n,Set(${HotExten}_PIN=${HOTDESK_INFO(pin,${HotExten})})
  same => n,Noop(${HotExten}_PIN is now ${${HotExten}_PIN})
  same => n,Noop(${HotExten}_STATUS is ${${HotExten}_STATUS})})
```
Мы еще не закончили написание этого расширения, но нам нужно отвлечься на несколько страниц, чтобы обсудить, где мы находимся на данном этапе.

Когда торговый агент садится за стол, он входит в систему, набирая \*99 плюс свой добавочный номер. В этом случае мы разрешили входя в систему расширениям с 1101 по 1105 посредством нашего шаблона соответствия `_99110[1-5]`. Вы могли бы так же легко сделать его менее ограниченным, используя `_9911XX` (разрешая с 1100 до 1199). Это расширение использует функцию `func_odbc` для выполнения поиска с помощью функции диалплана `HOTDESK_INFO()`. Эта пользовательская функция (которую мы определим в файле _func_odbc.conf_) выполняет инструкцию SQL и возвращает все, что извлекается из базы данных.

Итак, давайте создадим _/etc/asterisk/func_odbc.conf_, и в нем определим новую функцию HOTDESK_INFO():

```
$ sudo -u asterisk vim /etc/asterisk/func_odbc.conf

[INFO]
prefix=HOTDESK
dsn=asterisk
synopsis=Select value of field in ARG1, where 'extension' matches ARG2
description=Allow dialplan to extract data from any field in pbx.ast_hotdesk table.
readsql=SELECT ${ARG1} FROM pbx.ast_hotdesk WHERE extension = '${ARG2}'
```

Это очень много всего в нескольких строчках. Давайте быстро прикроем их, прежде чем двинемся дальше.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Примечание</b></p>
      <p>Вы должны быть в состоянии перезагрузить свой диалплан (<code>dialplan reload</code>) и <code>func_odbc</code> (<code>module reload func_odbc.so</code>), и протестировать диалплан до этого момента (наберите 991101 с одного из устройств, которые вы назначили этому контексту). Убедитесь, что ваша детальность консоли установлена по крайней мере на 3 (<code>*CLI> core set verbose 3</code>), так вы сможете увидеть, что этот диалплан работает только в консоли (вызов этого диалплана быстро вернет "занято", даже если он работает успешно). Для остальной части этого раздела мы настоятельно рекомендуем вам тестировать все после каждого изменения. Если вы этого не сделаете, у вас будет много времени для поиска ошибок. Очень важно, чтобы вы кодировали с зарегистрированным телефоном и открытой консолью Asterisk, чтобы перезагрузить и протестировать изменения в течение нескольких секунд после их написания.</p>
    </td>
  </tr>
</table>


Прежде всего, `prefix` является необязательным (префикс по умолчанию - `ODBC`). Это означает, что если вы не определяете `prefix`, Asterisk добавляет `ODBC` к имени функции (в данном случае `INFO`), означающее, что эта функция станет `ODBC_INFO()`. Это не очень удачно описывает то, что делает функция, поэтому может быть полезно назначить префикс, который помогает связать ваши функции ODBC с задачами, которые они выполняют. Мы выбрали '`HOTDESK`', означающее, что эта пользовательская функция в диалплане будет называться `HOTDESK_INFO()`.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Примечание</b></p>
      <p>Причина, по которой <code>prefix</code> является отдельным, заключается в том, что автор модуля хотел уменьшить возможные коллизии с существующими функциями диалплана. Цель <code>prefix</code> состояла в том, чтобы разрешить несколько копий одной и той же функции, подключенной к разным базам данных, для систем Asterisk с несколькими арендаторами. Мы, как авторы, были немного более либеральны в нашем использовании <code>prefix</code>, чем первоначально предполагал разработчик.</p>
    </td>
  </tr>
</table>

Атрибут `dsn` сообщает Asterisk, какое соединение использовать из _res_odbc.conf_. Поскольку в _res_odbc.conf_ можно настроить несколько подключений к базам данных, мы указываем, какой из них использовать здесь. На Рисунке 15-1 показана взаимосвязь между различными конфигурациями файлов и тем, как они ссылаются на цепочку для подключения к базе данных.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Подсказка</b></p>
      <p>Файл <i>func_odbc.conf.sample</i> в каталоге исходников Asterisk содержит дополнительную информацию о том, как обрабатывать несколько баз данных и управлять чтением и записью информации в различные соединения DSN. В частности, аргументы <code>readhandle</code>, <code>writehandle</code>, <code>readsql</code> и <code>writesql</code> обеспечивают большую гибкость для интеграции и управления базами данных.</p>
    </td>
  </tr>
</table>

Наконец, мы определяем наш оператор SQL с атрибутом `readsql`. Функции диалплана могут вызываться в двух различных форматах: один для получения информации, а другой для настройки. Атрибут `readsql` используется, когда мы вызываем функцию `HOTDESK_INFO()` с форматом поиска (мы могли бы выполнить отдельный оператор SQL с атрибутом `writesql`; мы обсудим формат для этого атрибута немного позже в этой главе).

Чтение значений из этой функции будет принимать этот формат в диалплане:

```
exten => s,n,Set(RETURNED_VALUE=${HOTDESK_INFO(status,1101)})
```

Это вернет значение, расположенное в базе данных в столбце `status`, где столбец `extension` равен `1101`. `status` и `1101`, которые мы передаем функции `HOTDESK_INFO()`, затем помещаются в инструкцию SQL, которую мы назначили атрибуту `readsql`, доступному как `${ARG1}` и `${ARG2}` соответственно. Если бы мы передавали третий параметр, то он был бы доступен как `${ARG3}`.

После выполнения инструкции SQL возвращаемое значение (если оно есть) присваивается переменной канала `RETURNED_VALUE`.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="center"><b>Использование функции ARRAY()</b></p>
      <p>В нашем примере мы используем два отдельных вызова базы данных и присваиваем эти значения паре переменных канала: <code>${HotdeskExtension}_STATUS</code> и <code>${HotdeskExtension}_PIN</code>. Это было сделано для упрощения примера. Мы собираемся сократить имена переменных здесь, потому что печатный формат не может обрабатывать такие длинные строки, поэтому в следующих примерах вы увидите "HE" вместо "HotdeskExtension". Если вы собираетесь использовать этот пример, пожалуйста, замените <code>HE</code> расширением <code>HotdeskExtension</code>:</p>
<p><pre><code>
  same => n,Set(${HE}_STATUS=${HOTDESK_INFO(status,${HE})})
  same => n,Set(${HE}_PIN=${HOTDESK_INFO(pin,${HE})})
</code></pre></p>
      <p>В качестве альтернативы мы могли бы вернуть несколько столбцов и сохранить их в отдельные переменные, используя функцию диалплана <code>Array()</code>. Если бы мы определили наш оператор SQL в функции <code>func_odbc.conf</code> следующим образом:</p>
<p><pre><code>
readsql=SELECT pin,status FROM ast_hotdesk WHERE extension = '${HE}'
</code></pre></p>
      <p>мы могли бы использовать функцию <code>ARRAY()</code> для сохранения каждого столбца информации для строки в отдельной переменной с помощью одного вызова базы данных (обратите внимание, что мы используем пример функции с именем <code>HOTDESK_INFO()</code>, которую мы не создали):</p>
<p><pre><code>
  same => n,Set(ARRAY(${HE}_PIN,${HE}_STATUS)=${HOTDESK_INFO(${HE})})
</code></pre></p>
      <p>Использование <code>ARRAY()</code> удобно в любое время, когда вы можете получить значения, разделенные запятыми, и хотите назначить значения отдельным переменным, например, с помощью <code>CURL()</code>. Однако это также может усложнить чтение, отладку и обслуживание кода.</p>
    </td>
  </tr>
</table>

Итак, в первых двух строках следующего блока кода мы передаем значение `status` и значение, содержащееся в переменной `${HotdeskExtension}` (например, `1101`) в функцию `HOTDESK_INFO()`. Затем эти два значения заменяются в инструкции SQL на `${ARG1}` и `${ARG2}` соответственно, и выполняется инструкция SQL. Наконец, возвращаемое значение присваивается переменной канала `${HotdeskExtension}_STATUS`.

Давайте закончим писать расширение для сопоставления прямо сейчас:

```
exten => _*99110[1-5],1,Noop(Hotdesk login)
  same => n,Set(HotdeskExtension=${EXTEN:3}) ; strip off the leading *99
  same => n,Noop(Hotdesk Extension ${HotdeskExtension} is changing status) ; for the log
  same => n,Set(${HotdeskExtension}_STATUS=${HOTDESK_INFO(status,${HotdeskExtension})})
  same => n,Set(${HotdeskExtension}_PIN=${HOTDESK_INFO(pin,${HotdeskExtension})})
  same => n,Noop(${HotdeskExtension}_PIN is now ${${HotdeskExtension}_PIN})
  same => n,Noop(${HotdeskExtension}_STATUS is ${${HotdeskExtension}_STATUS})})
  same => n,GotoIf($["${${HotdeskExtension}_PIN}" = ""]?invalid_user)
  same => n,GotoIf($[${ODBCROWS} < 0]?invalid_user)
  same => n,GotoIf($[${${HotdeskExtension}_STATUS} = 1]?logout:login,1)
```

Мы напишем некоторые метки для обработки `invalid_user` и `logout` немного позже, поэтому не волнуйтесь, если вам кажется, что чего-то не хватает.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Примечание</b></p>
      <p>Возможно, вы заметили, что в некоторых примерах <code>Goto/GotoIf</code> в директиве может быть <code>1</code>. Это может показаться запутанным, если только вы не вспомните, что для цели нужна только разница между текущим <code>context,extension,priority/label</code>. Таким образом, если вы отправляете что-то на метку, например <code>logout</code>, которая находится в том же расширении, вам не нужно указывать контекст и расширение, тогда как если вы отправляете вызов на расширение с именем <code>login</code> (все еще в том же контексте), вам нужно указать, что вы хотите, чтобы вызов перешел на метку/приоритет <code>1</code>. В предыдущем примере мы могли бы записать нашу директиву следующим образом:</p>
<p><pre><code>
... = 1] ? hotdesk,${EXTEN},logout : hotdesk,login,1
             ^same    ^same   ^diff    ^same   ^diff ^diff
</code></pre></p>
      <p>Другими словами, <code>true</code> переводит к контексту <code>[hotdesk]</code>, расширению <code>99110[1-5]</code>, метке <code>logout</code>; а <code>false</code> - к контексту <code>[hotdesk]</code>, расширению <code>login</code> и метке/приоритету <code>1</code>.</p>
      <p>Мы написали только то, что отличается.</p>
      <p>Если хотите, для ясности вы всегда можете указывать <code>context,extension,priority</code> для всех ваших директив. Это Ваш выбор.</p>
    </td>
  </tr>
</table>


После присвоения значения столбца `status` переменной `${HotdeskExtension}_STATUS` (если пользователь идентифицирует себя как расширение `1101`, имя переменной будет `1101_STATUS`), мы проверяем, получили ли значение обратно из базы данных, используя переменную канала `${ODBCROWS}`.

Последняя строка блока проверяет состояние телефона и, если агент в данный момент вошел в систему, выводит его оттуда. Если агент еще не вошел в систему, он перейдет к расширению входа.

При расширении входа в систему диалплан выполняет некоторые начальные проверки для подтверждения PIN-кода, введенного агентом. (Кроме того, мы использовали функцию `FILTER()`, чтобы убедиться, что были введены только числа для избежания некоторых проблем с SQL-инъекцией.) Мы разрешаем три попытки введения правильного PIN-кода, и если все попытки недействительны - разрываем связь:

```
exten => login,1,NoOp() ; set initial counter values
   same => n,Set(PIN_TRIES=1)     ; pin tries counter
   same => n,Set(MAX_PIN_TRIES=3) ; set max number of login attempts
   same => n,Playback(silence/1)  ; play back some silence so first prompt is
                                  ; not cut off
   same => n(get_pin),NoOp()
   same => n,Set(PIN_TRIES=$[${PIN_TRIES} + 1])   ; increase pin try counter
   same => n,Read(PIN_ENTERED,enter-password,${LEN(${${HotdeskExtension}_PIN})})
   same => n,Set(PIN_ENTERED=${FILTER(0-9,${PIN_ENTERED})})
   same => n,GotoIf($["${PIN_ENTERED}" = "${${HotdeskExtension}_PIN}"]?valid:invalid)
   same => n,Hangup()

   same => n(invalid),Playback(vm-invalidpassword)
   same => n,GotoIf($[${PIN_TRIES} <= ${MAX_PIN_TRIES}]?get_pin)
   same => n,Playback(goodbye)
   same => n,Hangup()

   same => n(valid),Noop(Valid PIN)
```

Если введенный PIN-код совпадает, мы продолжаем процесс входа в систему через метку (`valid`). Сначала используем переменную `CHANNEL`, чтобы выяснить, с какого телефонного устройства звонит агент. Переменная `CHANNEL` обычно заполняется чем-то похожим на `PJSIP/HOTDESK_1-ab4034c`, поэтому мы используем функцию `CUT()` сперва для удаления части строки строки `PJSIP/`. Затем удаляем часть строки `-ab4034c`, и то, что остается, - это то, что мы хотим (`HOTDESK_1`):<sup><a href="#sn5">5</a></sup>

```
same => n(valid),Noop(Valid PIN)
; CUT off the channel technology and assign it to the LOCATION variable
same => n,Set(LOCATION=${CUT(CHANNEL,/,2)})
; CUT off the unique identifier and save the remainder to the LOCATION variable
same => n,Set(LOCATION=${CUT(LOCATION,-,1)})
; we'll come back to this shortly
```

Мы собираемся создать и использовать еще несколько функций в файле _func_odbc.conf_: `HOTDESK_CHECK_SET()`, которая определит, назначены ли уже другие пользователи этому телефону; `HOTDESK_STATUS()`, которая назначит телефон этому агенту; и `HOTDESK_CLEAR_SET()`, которая очистит всех других пользователей, назначенных в данный момент этому телефону (которые, возможно, забыли выйти из системы).

В файле _func_odbc.conf_ нам нужно будет создать следующие функции:

```
; func_odbc.conf
[CHECK_SET]
prefix=HOTDESK
dsn=asterisk
synopsis=Check if this set is already assigned to somebody.
readsql=SELECT COUNT(status) FROM pbx.ast_hotdesk WHERE status = '1'
readsql+= AND endpoint = '${ARG1}'

[STATUS]
prefix=HOTDESK
dsn=asterisk
synopsis=Assign hotdesk extension to this endpoint/set.
writesql=UPDATE pbx.ast_hotdesk SET status = '${SQL_ESC(${VAL1})}',
writesql+= endpoint = '${SQL_ESC(${VAL2})}'
writesql+= WHERE extension = '${SQL_ESC(${ARG1})}'

[CLEAR_SET]
prefix=HOTDESK
dsn=asterisk
synopsis=Clear all instances of this endpoint
writesql=  UPDATE pbx.ast_hotdesk SET status=0,endpoint=NULL
writesql+= WHERE endpoint='${SQL_ESC(${VAL1})}'
```

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Подсказка</b></p>
      <p>Из-за ограничений длины строк в книге мы разбили команды <code>readsql</code> и <code>writesql</code> на несколько строк, используя синтаксис <code>+=</code>, который говорит Asterisk добавлять содержимое после <code>readsql+=</code> к самому последнему определенному значению <code>readsql=</code> (или <code>writesql</code> и <code>writesql+</code>). Использование <code>+=</code> применимо не только к опции <code>readsql</code>, но и может использоваться в других местах в других файлах <i>.conf</i> внутри Asterisk.</p>
    </td>
  </tr>
</table>

В нашем диалплане нам нужно будет вызвать функцию, которую мы только что создали, и передать поток вызовов метке `forcelogout`, если кто-то уже вошел в это устройство:

```
    same => n(valid),Noop(Valid PIN)
    same => n,Set(LOCATION=${CUT(CHANNEL,/,2)})
    same => n,Set(LOCATION=${CUT(LOCATION,-,1)})
; We'll come back to this shortly ; you can remove this comment/line
    same => n(checkset),Set(SET_USED=${HOTDESK_CHECK_SET(${LOCATION})})
    same => n,GotoIf($[${SET_USED} > 0]?forcelogout)

; Set status for agent  to '1' and update the location/endpoint
    same => n(set_login_status),Set(HOTDESK_STATUS(${HotdeskExtension})=1,${LOCATION})
    same => n,Noop(ODBCROWS is ${ODBCROWS})
    same => n,GotoIf($[${ODBCROWS} < 1]?error,1)
    same => n,Playback(agent-loginok)
    same => n,Hangup()

    same => n(forcelogout),NoOp()
; set all currently logged-in users on this device to logged-out status
    same => n,Set(HOTDESK_CLEAR_SET()=${LOCATION})
    same => n,Goto(checkset)      ; return to logging in
```

Есть некоторые потенциально новые концепции, которые мы только что представили в примерах. В частности, синтаксис функции `HOTDESK_STATUS()` содержит несколько новых трюков, которые вы могли заметить. Теперь у нас есть переменные `${Valx}` и `${ARGx}` в нашем операторе SQL.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Примечание</b></p>
      <p>Мы также завернули значения <code>${Valx}</code> и <code>${ARGx}</code> в функцию <code>SQL_ESC()</code>, которая будет экранировать символы, такие как обратные кавычки, которые могут быть использованы в атаке SQL-инъекцией.</p>
    </td>
  </tr>
</table>

Они содержат информацию, которую мы передаем функции из диалплана. В этом случае у нас есть две переменные `VAL` и одна переменная `ARG`, которые были установлены из диалплана с помощью этого оператора:

```
same => n(set_login_status),Set(HOTDESK_STATUS(${HotdeskExtension})=1,${LOCATION})
```

Обратите внимание, что синтаксис немного отличается от синтаксиса функции чтения. Это сигнализирует Asterisk, что вы хотите выполнить запись (это тот же структурный синтаксис, что и для других функций диалплана).

Мы включаем значение переменной `${HotdeskExtension}` в наш вызов функции `HOTDESK_STATUS()` (которая затем становится переменной `${ARG1}` для этой функции в _func_odbc.conf_). Однако мы также передаем два значения, '`1`' и `${LOCATION}`. Они будут связаны в функции переменными `${VAL1}` и `${VAL2}` соответственно.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="center"><b>Использование SQL непосредственно в диалплане</b></p>
      <p>Некоторые предпочитают писать свои SQL-операторы непосредственно в диалплане, а не создавать пользовательскую функцию для каждого типа транзакции базы данных, которую они могут захотеть выполнить.</p>
      <p>Теоретически, вы можете создать только одну функцию в <i>func_odbc.conf</i> как эта:</p>
<p><pre><code>
[SQL]
prefix=GENERIC
dsn=asterisk
readsql=${SQL_ESC(${ARG1})}
writesql=${SQL_ESC(${VALUE})} ; Целое значение, необработанное
</code></pre></p>
      <p>Затем в диалплане вы можете написать практически любой тип SQL, который захотите (при условии, что ODBC-коннектор может обрабатывать его, что не имеет никакого отношения к Asterisk). Эта функция выше затем отправит любую строку, которую вы указали непосредственно в соединение ODBC с вашей базой данных.<sup><a href="#sn6">6</a></sup></p>
      <p>Некоторые утверждают, что это приводит к большей путанице в вашем диалплане; другие будут настаивать на том, что преимущество наличия гораздо более простого <i>func_odbc.conf</i> стоит того:</p>
<p><pre><code>
  same => n,Set(result=${GENERIC_SQL(SELECT col FROM table WHERE ...)})
  same => n,Verbose(1,${result})<br>
  same => n,Set(GENERIC_SQL()=UPDATE table SET field="VAL" WHERE ...)
  same => n,Verbose(1,ODBC_RESULT is ${OBDBC_RESULT})
</code></pre></p>
      <p>Мы считаем, что в целом лучше создавать конкретные функции в _func_odbc.conf_ для обработки запросов из вашего диалплана; тем не менее, нет никакого соблазна использовать одну функцию для обработки всех запросов SQL.</p>
    </td>
  </tr>
</table>


<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="center"><b>Многорядная функциональность с func_odbc</b></p>
      <p>Asterisk имеет многорядный режим, который позволяет ему обрабатывать несколько строк данных, возвращаемых из базы данных. Например, если бы мы создали функцию диалплана в <i>func_odbc.conf</i>, которая возвращает все доступные расширения, нам нужно было бы включить режим мультистрочности для функции. Это заставило бы функцию работать немного по-другому, возвращая идентификационный номер, который затем можно было бы передать функции <i>ODBC_FETCH()</i> для возврата каждой строки по очереди.</p>
      <p>Далее следует простой пример. Предположим, что у нас есть следующий <i>func_odbc.conf</i>:</p>
<p><pre><code>
[AVAILABLE_EXTENS]
prefix=HOTDESK
dsn=asterisk
mode=multirow
readsql=SELECT extension FROM ast_hotdesk WHERE status = '${ARG1}'
</code></pre></p>
      <p>и диалплан в <i>extensions.conf</i>, выглядящий примерно так:</p>
<p><pre><code>
exten => *9997,1,Noop(multirow)
  same => n,Set(ODBC_ID=${HOTDESK_AVAILABLE_EXTENS()})
  same => n,GotoIf($[${ODBCROWS} < 1]?no_rows)
  same => n,Answer()
  same => n,Set(COUNTER=1)
  same => n,While($[${COUNTER} <= ${ODBCROWS}])
  same => n,Set(AVAIL_EXTEN_${COUNTER}=${ODBC_FETCH(${ODBC_ID})})
  same => n,SayDigits(${AVAIL_EXTEN_${COUNTER}})
  same => n,Wait(0.2) ; Pause between speaking
  same => n,Set(COUNTER=$[${COUNTER} + 1])
  same => n,EndWhile()
  same => n(norows),ODBCFinish()
  same => n,Hangup()
</code></pre></p>
      <p>Обратите внимание, что если у вас нет нескольких конечных точек для входа в систему, это никогда не вернет более одного расширения в вашей лаборатории, потому что только одно устройство будет входить в систему в любое время. Вы можете добавить некоторые фиктивные данные в таблицу, чтобы просто посмотреть, как это работает:</p>
<p><pre><code>
MySQL> UPDATE pbx.ast_hotdesk
       SET status='1',endpoint='HOTDESK_2'
       WHERE id='3'
       ;
MySQL> UPDATE pbx.ast_hotdesk
       SET status='1',endpoint='HOTDESK_3'
       WHERE id='5'
       ;
</code></pre></p>
      <p>Функция <code>ODBC_FETCH()</code> по существу будет обрабатывать информацию как стек, и каждый вызов к ней с переданным <code>ODBC_ID</code> будет выводить следующую строку информации из стека. У нас также есть возможность использовать переменную канала <code>ODBC_FETCH_STATUS</code>, которая устанавливается после вызова функции <code>ODBC_FETCH()</code> (которая возвращает <code>SUCCESS</code> - если доступны дополнительные строки, или <code>FAILURE</code> - если нет дополнительных строк). Это позволяет нам написать диалплан, подобный приведенному ниже, использующий счетчик, но все же циклически перебирающий данные. Это может быть полезно, если мы ищем что-то конкретное и не нужно просматривать все данные. Как только мы закончим, приложение диалплана <code>ODBCFinish()</code> должно быть вызвано для очистки всех оставшихся данных.</p>
      <p>Вот еще один пример <i>extensions.conf</i>:</p>
<p><pre><code>
[multirow_example_2]
exten => start,1,Verbose(1,Looping example with break)
  same => n,Set(ODBC_ID=${GET_ALL_AVAIL_EXTENS(1)})
  same => n(loop_start),NoOp()
  same => n,Set(ROW_RESULT=${ODBC_FETCH(${ODBC_ID})})
  same => n,GotoIf($["${ODBC_FETCH_STATUS}" = "FAILURE"]?cleanup,1)
  same => n,GotoIf($["${ROW_RESULT}" = "1104"]?good_exten,1)
  same => n,Goto(loop_start)
<br>
exten => cleanup,1,Verbose(1,Cleaning up after all iterations)
  same => n,Verbose(1,We did not find the extension we wanted)
  same => n,ODBCFinish(${ODBC_ID})
  same => n,Hangup()
<br>
exten => good_exten,1,Verbose(1,Extension we want is available)
  same => n,ODBCFinish(${ODBC_ID})
  same => n,Verbose(1,Perform some action we wanted)
  same => n,Hangup()
</code></pre></p>
    </td>
  </tr>
</table>

Ладно, мы немного отклонились от темы. Давайте завершим несколько частей компонентов агента, которые еще не обработали.

В расширении `_*99110[1-5]` нам нужны следующие метки:
```
  same => n,GotoIf($[${${HotdeskExtension}_STATUS} = 1]?logout:login,1)

  same => n(invalid_user),Noop(Hot Desk extension ${HotdeskExtension} does not exist)
  same => n,Playback(silence/2&login-fail)
  same => n,Hangup()

  same => n(logout),Noop()
  same => n,Set(HOTDESK_STATUS(${HotdeskExtension})=0,) ; Note VAL2 is empty
  same => n,GotoIf($[${ODBCROWS} < 1]?error,1)
  same => n,Playback(silence/1&agent-loggedoff)
  same => n,Hangup()
```
Мы также включаем контекст `hotdesk_outbound`, который будет обрабатывать исходящие вызовы после того, как мы зарегистрируем агента в системе:
```
include => hotdesk_outbound ; эта строка может быть в любом месте контекста [hotdesk]
```
Контекст `[hot desk_outbound]` использует многие из тех же принципов, которые уже обсуждались. Он использует совпадение шаблонов для перехвата любых номеров, набранных с телефонов горячей линии. Сначала мы устанавливаем нашу переменную  `LOCATION` с помощью переменной `CHANNEL`, затем определяем какое расширение (агент) зарегистрировано в системе и присваиваем это значение переменной `WHO`. Если эта переменная имеет значение `NULL`, мы отклоняем исходящий вызов. Если оно не равно `NULL`, то мы получаем информацию об агенте с помощью функции `HOTDECK_INFO()` и присваиваем ее нескольким переменным `CHANNEL`.
```
include => hotdesk_outbound

; put this code right below your [hotdesk] context
[hotdesk_outbound]
exten => _NXXXXXX.,1,NoOp()
 same => n,Set(LOCATION=${CUT(CHANNEL,/,2)})
 same => n,Set(LOCATION=${CUT(LOCATION,-,1)})
 same => n(checkset),Set(VALID_AGENT=${HOTDESK_CHECK_SET(${LOCATION})})
 same => n,Noop(VALID_AGENT is ${VALID_AGENT})
 same => n,Set(${CALLERID(name)}=${HOTDESK_INFO(cid_name,${VALID_AGENT})})
 same => n,Set(${CALLERID(num)}=${HOTDESK_INFO(cid_number,${VALID_AGENT})})
 same => n,GotoIf($[${VALID_AGENT} = 0]?notallowed) ; Nobody logged in--calls not allowed
 same => n,Dial(${LOCAL}/${EXTEN}) ; See the Outside Connectivity chapter
 same => n,Hangup()

 same => n(notallowed),Playback(sorry-cant-let-you-do-that2)
 same => n,Hangup()
```
Если вы не вошли в систему, вызов завершится ошибкой с сообщением. Если вы вошли в систему, вызов будет передан приложению `Dial()` (которое также может завершиться неудачей, если у вас нет настроенного оператора связи, но это описывается в предыдущих главах, поэтому мы оставим это в том разделе).

Нам требуется еще один последний бит диалплана. Мы создали эту сложную среду, которая позволяет нашим агентам входить и выходить, но на самом деле нет никакого способа вызвать их!

Мы собираемся исправить это сейчас, сделав четыре вещи:

1. Мы собираемся включить контекст `[sets]` в контекст `[hotdesk]`, чтобы наши агенты могли использовать другие части нашего диалплана.
2. Мы собираемся дать нашим агентам почтовые ящики.
3. Мы создадим новую подпрограмму, которая будет проверять службу поддержки на наличие агента и a) звонить им, если они там есть, или b) отправлять вызов на голосовую почту, если их нет.
4. Мы собираемся построить диалплан в контексте `[sets]`, чтобы каждый мог позвонить нашим агентам.

Давайте сначала уберем почтовые ящики:
```
MySQL> insert into `asterisk`.`voicemail`
(mailbox,fullname,context,password)
VALUES
('1101','Herb Tarlek','default','110111'),
('1102','Al Bundy','default','110222'),
('1103','Willy Loman','default','110333'),
('1104','Jerry Lundegaard','default','110444'),
('1105','Moira Brown','default','110555');
```
Вся остальная работа заключается в _extensions.conf_:

Далеко внизу, в самом низу, давайте создадим подпрограмму, которая будет обрабатывать все для нас:
```
[subDialHotdeskUser]
exten => _[a-zA-Z0-9].,1,Noop(Call Hotdesk)
 same => n,Set(HOTDESK_ENDPOINT=${HOTDESK_INFO(endpoint,${EXTEN})}) ; Get assigned device
 same => n,GotoIf($["${HOTDESK_ENDPOINT}" = ""]?voicemail) ; if blank, send to voicemail
 same => n(ringhotdesk),Dial(PJSIP/${HOTDESK_ENDPOINT},${ARG1})
 same => n(voicemail),Voicemail(${EXTEN})
 same => n,Hangup()
```
И где-то гораздо ближе к началу, мы добавим наших пользователей горячего стола в раздел диалплана, где живут наши другие пользователи:
```
exten => 110,1,Dial(${UserA_DeskPhone}&${UserA_SoftPhone}&${UserB_SoftPhone})

exten => 1101,1,GoSub(subDialHotdeskUser,${EXTEN},1(12))
exten => 1102,1,GoSub(subDialHotdeskUser,${EXTEN},1(12))
exten => 1103,1,GoSub(subDialHotdeskUser,${EXTEN},1(12))
exten => 1104,1,GoSub(subDialHotdeskUser,${EXTEN},1(12))
exten => 1105,1,GoSub(subDialHotdeskUser,${EXTEN},1(12))

exten => 200,1,Answer()
     same => n,Playback(hello-world)
     same => n,Hangup()
```
И наконец, вернувшись в наш контекст `[hotdesk]`, мы позволим нашим агентам использовать остальную часть телефонной системы:
```
[hotdesk]

include => sets

exten => _*99110[1-5],1,Noop(Hotdesk login)
```
Попробуйте несколько сценариев:

1. Вызов от внутреннего агента.
2. Вызов от обычного пользователя к зарегистрированному агенту.
3. Вызов от обычного пользователя к недоступному агенту.

Поразитесь этому технологическому террору, который вы создали.

Теперь, когда мы реализовали довольно сложную функцию в диалплане, используя `func_odbc` для извлечения и хранения данных в удаленной реляционной базе данных, вы можете увидеть как с помощью нескольких довольно простых функций в файле _func_odbc.conf_ и нескольких таблиц в базе данных можно создать несколько мощных приложений телефонии.

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

## Использование Realtime

Архитектура Asterisk Realtime (ARA) позволяет хранить все параметры, обычно хранящиеся в конфигурационных файлах Asterisk (обычно располагающихся в  _/etc/asterisk_), в базе данных. Существует два типа realtime: статический и динамический.

Статическая версия аналогична традиционному способу чтения конфигурационного файла (информация загружается только при запуске из CLI), за исключением того, что вместо этого данные считываются из базы данных.<sup><a href="#sn7">7</a></sup>

Динамический realtime загружает и обновляет информацию по мере ее использования живой системой, обычно используется для таких вещей, как SIP (или IAX2 и т.д.) пользователи и пиры, а также ящики голосовой почты.

Внесение изменений в статическую информацию требует перезагрузки, как если бы вы изменили текстовый файл в системе, но динамическая информация опрашивается Asterisk по мере необходимости, поэтому при внесении изменений в эти данные перезагрузка не требуется. Realtime настраивается в файле _extconfig.conf_ находящемся в каталоге _/etc/asterisk_. Этот файл сообщает Asterisk, что и откуда нужно грузить из базы данных, позволяя загружать определенные данные из базы данных, а другие - из стандартных файлов конфигурации.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Подсказка</b></p>
      <p>Другим (возможно, более старым) способом хранения конфигурации Asterisk был внешний скрипт, который взаимодействовал бы с базой данных и генерировал соответствующие плоские файлы (или <i>.conf</i> файлы), а затем перезагружал соответствующий модуль после того, как новый файл был записан. В этом есть преимущество (если база данных выйдет из строя, ваша система будет продолжать функционировать; скрипт просто не будет обновлять файлы до тех пор, пока не будет восстановлено подключение к базе), но у него также есть недостатки. Одним из основных недостатков является то, что любые изменения, внесенные пользователем, будут недоступны до запуска скрипта обновления. Это, вероятно, не является большой проблемой для небольших систем, но на нагруженных системах ожидание применения изменений может вызвать проблемы, такие как приостановка вызова во время загрузки и анализа большого файла.</p>
      <p>Вы можете частично избавиться от этого, используя реплицированную систему баз данных. Asterisk предоставляет возможность аварийного переключения на другую систему баз данных. Таким образом, вы можете кластеризировать бэкэнд базы данных, используя отношения master-master (для PostgreSQL, pgcluster или Postgre-R;<sup><a href="#sn8">8</a></sup> для MySQL это встроено <sup><a href="#sn9">9</a></sup>) или master-slave (для PostgreSQL или <a href="http://www.slony.info/">Slony-I</a>; для MySQL это встроено) системы репликации.</p>
      <p>Наш неофициальный обзор таких вещей показывает, что использование скриптов для записи плоских файлов из баз данных не так популярно, как запрос базы данных в реальном времени (и обеспечение базы данных с достаточной степенью отказоустойчивости для обработки того факта, что живая телекоммуникационная система зависит от нее).</p>
      </td>
  </tr>
</table>

### Статический Realtime

Статический realtime был одним из самых ранних способов хранения конфигурации Asterisk в базе данных. Он все еще несколько полезен для хранения простых конфигурационных файлов в базе данных (которые обычно можно поместить в _/etc/asterisk_). Мы больше не склонны использовать его, потому что динамический realtime намного лучше для больших наборов данных, а файлы конфигурации на основе файлов более чем адекватны для небольших параметров конфигурации.

Те же правила, которые применяются к плоским файлам в вашей системе, по-прежнему применяются при использовании статического realtime. Например, после внесения изменений в конфигурацию вам все равно придется выполнить команду `module reload` для соответствующей технологии (например, `*CLI> module reload res_musiconhold.so`).

При использовании статического realtime мы сообщаем Asterisk, какие файлы хотим загрузить из базы данных, используя следующий синтаксис в файле _extconfig.conf_:
```
; /etc/asterisk/extconfig.conf
[settings]
filename.conf => driver,database[,table]
```

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="left"><b>Примечание</b></p>
      <p>Нет никакого конфигурационного файла с именем <i>filename.conf</i>. Вместо этого используйте фактическое имя файла конфигурации, который вы храните в базе данных. Если имя таблицы не указано, Asterisk будет использовать имя файла в качестве имени таблицы вместо этого (за вычетом части <i>.conf</i>). Кроме того, все настройки внутри <i>extconfig.conf</i> должны находиться под заголовком <code>[settings]</code>. Имейте в виду, что вы не можете загружать определенные файлы из realtime в принципе, включая <i>asterisk.conf</i>, <i>extconfig.conf</i> и <i>logger.conf</i>.</p>
    </td>
  </tr>
</table>

Модуль статического realtime использует очень специфично отформатированную таблицу, позволяющую Asterisk считывать различные статические файлы из базы данных. Таблица 15-1 иллюстрирует столбцы, как они должны быть определены в вашей базе данных.

_Таблица 15-1. Макет таблицы и описание ast_config_

| Имя столбца | Тип столбца | Описание |
| :--- | :--- | :--- |
| `id` | Serial, автоувеличивающийся | Автоувеличивающееся уникальное значение для каждой строки таблицы. |
| `cat_metric` | Integer | Вес категории внутри файла. Более низкая метрика означает что она отображается выше в файле (см. врезку). |
| `var_metric` | Integer | Вес в пределах категории. Более низкая метрика означает, что она отображается выше в списке (см. врезку). Это полезно для таких вещей, как порядок кодеков в _sip.conf_, или _iax.conf_, в котором `disallow=all` должно быть первым (показатель `0`), затем `allow=ulaw` (метрика `1`), а затем `allow=gsm` (метрика `2`). |
| `filename` | Varchar 128 | Имя файла, которое модуль обычно считывает с жесткого диска вашей системы (например _musiconhold.conf_, _sip.conf_, _iax.conf_). |
| `category` | Varchar 128 | Имя раздела в файле, например `[general]`. Не заключайте имя в квадратные скобки при сохранении в базу данных. |
| `var_name` | Varchar 128 | Параметр слева от знака равенства (например, `disallow` - это `var_name` в `disallow=all`). |
| `var_val` | Varchar 128 | Параметр справа от знака равенства(например `all` - это `var_val` в `disallow=all`). |
| `commented` | Integer | Любое значение, отличное от 0, будет читаться так, как если бы оно было префиксировано точкой с запятой в файле конфигурации (закомментировано). |

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="center"><b>Несколько слов о метриках</b></p>
      <p>Метрики в статическом realtime используются для управления порядком считывания объектов в память. Представьте себе <code>cat_metric</code> и <code>var_metric</code> как исходные номера строк в файле конфигурации. Сначала обрабатывается более высокое значение <code>cat_metric</code>, поскольку Asterisk сопоставляет категории снизу вверх. В пределах одной категории, хотя, нижний <code>var_metric</code> обрабатывается первым, потому что Asterisk обрабатывает варианты "сверху-вниз" (например для <code>disallow=all</code> должно быть установлено значение ниже, чем значение <code>allow</code> в категории, чтобы убедиться, что оно обрабатывается в первую очередь).</p>
      </td>
    </tr>
</table>

О статическом realtime сказать больше нечего. Он был очень полезен в прошлом, но теперь в основном вытеснен динамическим realtime. Если вы хотите прочитать о нём подробнее, то это рассматривается в более старых версиях этой книги.

### Динамический Realtime

Система динамического realtime используется для загрузки объектов, которые могут часто изменяться, как например объекты PJSIP, очереди и их участники, а также голосовая почта. По мере того как будут добавляться новые записи на регулярной основе, мы можем использовать возможности базы данных, чтобы позволить нам загружать эту информацию по мере необходимости.

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

Весь realtime настраивается в файле _/etc/asterisk/extconfig.conf_; однако динамический режим realtime имеет четко определенные имена конфигурации. Все предопределенные имена должны быть настроены под заголовком `[settings]`. Например, определение одноранговых узлов (пиров) SIP выполняется с использованием следующего формата:

```
; extconfig.conf
[settings]
sippeers => driver,database[,table]
```

Имя таблицы является необязательным. Если оно опущено, Asterisk будет использовать предопределенное имя (т.е. `sippeers`) для определения таблицы, в которой будут искаться данные.

Пример файла _~/src/asterisk-15.\<TAB\>/configs/samples/extconfig.conf.sample_ содержит отличную информацию о динамическом realtime.

## Хранение записей деталей вызовов (CDR)

Записи деталей вызовов (CDRS) содержат информацию о вызовах, прошедших через вашу систему Asterisk. Они рассматриваются далее в [Главе 21](glava-21.md#записи-деталей-вызовов-cdr). Хранение CDR - это популярное использование баз данных в Asterisk, потому что оно облегчает работу с ними. Кроме того, помещая записи в базу данных, вы открываете множество возможностей, включая создание собственного веб-интерфейса для отслеживания статистики, такой как использование вызовов и наиболее часто вызываемых назначений, биллинга счетов или проверка счетов телефонной компании.

Вы всегда должны реализовывать хранение CDR в базе данных на любой производственной системе (вы всегда можете хранить CDR в файле, так что ничего не потеряно).

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td>
      <p align="center"><b>Настройка системного имени для глобальных Unique ID</b></p>
      <p>CDR состоит из уникального идентификатора и нескольких полей информации о вызове (включая исходный и целевой каналы, длину вызова, последнее выполненное приложение и т.д.). В кластеризованном наборе блоков Asterisk теоретически возможно дублирование уникальных идентификаторов, поскольку каждая система Asterisk учитывает только себя. Чтобы решить эту проблему, мы можем автоматически добавить системный идентификатор к передней части уникальных идентификаторов, добавив опцию в <i>etc/asterisk/asterisk.conf</i>. Для каждого из ваших блоков задайте идентификатор, добавив что-то вроде:</p>
<p><pre><code>
[options]
<br>
systemname=toronto
</code></pre></p>
    </td>
  </tr>
</table>

Лучший способ хранения записей подробных вызовов - это модуль `cdr_adaptive_odbc`. Он позволяет вам выбрать, какие столбцы данных, встроенных в Asterisk, хранятся в вашей таблице и позволяет добавлять дополнительные столбцы, которые могут быть заполнены функцией диалплана `CDR()`. Вы даже можете хранить разные части данных CDR в разных таблицах и базах данных, если это необходимо.

Чтобы создать таблицу, у нас есть Alembic. Этот процесс практически идентичен тому, который вы выполняли во время установки системы, за исключением, конечно, самого процесса отличается так же и .ini-файл.

```
$ cd ~/src/asterisk-15.<TAB>/contrib/ast-db-manage

$ cp cdr.ini.sample cdr.ini

$ egrep ^sqlalchemy config.ini

sqlalchemy.url = mysql://asterisk:YouNeedAReallyGoodPasswordHereToo@localhost/asterisk
```

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

```
$ sudo vim cdr.ini
```

Добавьте строку, которую вы только что получили от grep, в этот файл и сохраните.

```
$ alembic -c ./cdr.ini upgrade head

INFO  [alembic.runtime.setup] Creating new alembic_version_cdr table.
INFO  [alembic.runtime.migration] Running upgrade  -> 210693f3123d, Create CDR table.
INFO  [alembic.runtime.migration] Running upgrade 210693f3123d -> 54cde9847798
```

Alembic не слишком многословен, поэтому результат будет скупой, но, похоже, он успешно завершен. Давайте проверим.

```
$ mysql -u asterisk -p

MySQL&gt; describe asterisk.cdr
```

Вы должны получить список всех полей в таблице (что означает, что Alembic выполнился успешно). Если вы получите сообщение типа `Table 'asterisk.cdr' doesn't exist` - это указывает на что Alembic не завершил настройку, и вам нужно просмотреть сообщения с вывода Alembic'а, чтобы увидеть, что пошло не так (обычно ошибка в учетных данных).

Ну, это было не так уж и трудно, правда? Следующий шаг - указать Asterisk, чтобы он использовал эту новую таблицу для CDR в будущем.

```
$ sudo -u asterisk touch /etc/asterisk/cdr_adaptive_odbc.conf

$ sudo -u asterisk vim /etc/asterisk/cdr_adaptive_odbc.conf
```

В этот новый файл вставьте следующее:

```
[adaptive_connection]
connection=asterisk
table=cdr
```

Это довольно просто, неправда ли? Отлично, теперь нам просто нужно перезагрузить модуль `cdr_adaptive_odbc.so` в Asterisk:

```
$ sudo asterisk -rvvvvvvv

*CLI> module reload cdr_adaptive_odbc.so
```

Вы можете проверить, что адаптивный сервер ODBC был загружен, выполнив следующие действия:<sup><a href="glava-15.md#sn10">[10]</a></sup>

```
*CLI> cdr show status

Call Detail Record (CDR) settings
----------------------------------
  Logging:                    Enabled
  Mode:                       Simple
  Log unanswered calls:       No
  Log congestion:             No

* Registered Backends
  -------------------
    cdr-syslog
    Adaptive ODBC
    cdr-custom
    csv
    cdr_manager
```

Теперь сделайте вызов, на который будет получен ответ (например используя функции `Playback()` или `Dial()` в другом канале и ответив на него). Вы должны получить некоторые CDR, сохранившиеся в вашей базе данных. Вы можете проверить это, запустив команду `SELECT * FROM CDR;` из консоли базы данных.

При наличии основной информации CDR, хранящейся в базе данных, вам может потребоваться добавить в таблицу `cdr` дополнительную информацию, такую как стоимость маршрута. Вы можете использовать директиву `ALTER TABLE` для добавления столбца называемого `route_rate` к таблице:

```
sql> ALTER TABLE cdr ADD COLUMN route_rate varchar(10);
```

Теперь перезагрузите модуль `cdr_adaptive_odbc.so` из консоли Asterisk:

```
*CLI> module reload cdr_adaptive_odbc.so
```

и заполните новый столбец из диалплана Asterisk с помощью функции `CDR()`, например:

```
exten => _NXXNXXXXXX,1,Verbose(1,Example of adaptive ODBC usage)
   same => n,Set(CDR(route_rate)=0.01)
   same => n,Dial(SIP/my_itsp/${EXTEN})
   same => n,Hangup()
```

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

```
+--------------+----------+---------+------------+
| src          | duration | billsec | route_rate |
+--------------+----------+---------+------------+
| 0000FFFF0008 | 37       | 30      | 0.01       |
+--------------+----------+---------+------------+
```

На самом деле сохранение стоимости в записи вызовов может быть неидеальным (CDR обычно используется в качестве исходного ресурса, а такие вещи, как тарифы, добавляются ниже по потоку с помощью программного обеспечения для биллинга). Возможность добавления настраиваемых полей в CDR очень полезна, но будьте осторожны, чтобы не использовать записи вызовов для замены надлежащей платформы биллинга. Лучше всего сохранить ваш CDR чистым и сделать дальнейшую обработку ниже.

<table border="1" width="100%" cellpadding="5">
  <tr>
    <td><p align="center"><b>Дополнительные параметры конфигурации для cdr_adaptive_odbc.conf</b></p>
    <p>Некоторые дополнительные параметры конфигурации существуют в файле <i>cdr_adaptive_odbc.conf</i>, которые можгут быть полезны. Во-первых, вы можете определить несколько баз данных или таблиц для хранения информации, поэтому, если у вас есть несколько баз данных, которым нужна одна и та же информация, вы можете просто определить их в <i>res_odbc.conf</i>, создать таблицы в базах данных, а затем обращаться к ним в отдельных разделах конфигурации:</p>
<p><pre><code>
[mysql_connection]
connection=asterisk_mysql
table=cdr<br>
[mssql_connection]
connection=production_mssql
table=call_records
</code></pre></p>
    <hr>
    <p><b>Примечание</b></p>
    <p>Если вы зададите несколько разделов, используя одно и то же соединение и таблицу, то получите дублирующиеся записи.</p>
    <hr>
    <p>Помимо простой настройки нескольких соединений и таблиц (которые, конечно, могут содержать или не содержать одну и ту же информацию; модуль CDR, который мы используем, адаптирован к подобным ситуациям), мы можем определить псевдонимы для встроенных переменных, таких как <code>accountcode</code>, <code>src</code>, <code>dst</code> и <code>billsec</code>.</p>
    <p>Если бы мы добавили псевдонимы для имен столбцов для нашего соединения MS SQL, мы могли бы изменить наше определение соединения следующим образом:</p>
<p><pre><code>
[mssql_connection]
connection=production_mssql
table=call_records
alias src => Source
alias dst => Destination
alias accountcode => AccountCode
alias billsec => BillableTime
</code></pre></p>
    <p>В некоторых ситуациях можно указать соединение, в котором требуется регистрировать вызовы только из определенного источника или в определенное место назначения. Мы можем сделать это с помощью фильтров:</p>
<p><pre><code>
[logging_for_device_0000FFFF0008]
connection=asterisk_mysql
table=cdr_for_0000FFFF0008
filter src => 0000FFFF0008
</code></pre></p>
    <p>Если вам нужно заполнить определенный столбец информацией, основанной на имени раздела, вы можете установить его статически с помощью параметра <code>static</code>, который вы можете использовать с параметром <code>filter</code>:</p>
<p><pre><code>
[mysql_connection]
connection=asterisk_mysql
table=cdr
<br>
[filtered_mysql_connection]
connection=asterisk_mysql
table=cdr
filter src => 0000FFFF0008
static "DoNotCharge" => accountcode
</code></pre></p>
    <hr>
    <b>Примечание</b>
    <p>В предыдущем примере вы получите повторяющиеся записи в той же таблице, но вся информация будет одинаковой, за исключением заполненного столбца <code>accountcode</code>, поэтому вы должны иметь возможность отфильтровать его с помощью SQL.</p>
    <hr>
    </td>
  </tr>
</table>

## Интеграция базы данных с очередями

С call-центром (часто называемым очередями) может быть очень полезно иметь возможность разрешить настройку параметров очереди без необходимости редактирования и перезагрузки конфигурационных файлов. Управление call-центром может быть сложной задачей, а возможность более простой настройки параметров может сделать жизнь каждого человека намного проще.

Сами очереди мы уже разместили в базе данных в [Главе 12](glava-12.md). Однако если вы также хотите сохранить параметры диалплана, относящиеся к вашим очередям, база данных также может это сделать.

### Хранение параметров диалплана для очереди в базе данных

Приложение диалплана `Queue()` позволяет передавать в нее несколько параметров. Команда CLI `core show Application Queue` определяет следующий синтаксис:

```
[Syntax]
Queue(queuename[,options[,URL[,announceoverride[,timeout[,AGI[,macro[,gosub[,
  rule[,position]]]]]]]]])
```

Поскольку мы храним нашу очередь в базе данных, почему бы также не сохранить параметры, которые вы хотите передать в очередь аналогичным образом?
<sup><a href="#sn11">11</a></sup>

```
MySQL> CREATE TABLE `pbx`.`QueueDialplanParameters` (
  `QueueDialplanParametersID` mediumint(8) NOT NULL auto_increment,
  `Description` varchar(128) NOT NULL,
  `QueueID` mediumint(8) unsigned NOT NULL COMMENT 'Pointer to asterisk.queues table',
  `options` varchar(45) default 'n',
  `URL` varchar(256) default NULL,
  `announceoverride` bit(1) default NULL,
  `timeout` varchar(8) default NULL,
  `AGI` varchar(128) default NULL,
  `macro` varchar(128) default NULL,
  `gosub` varchar(128) default NULL,
  `rule` varchar(128) default NULL,
  `position` tinyint(4) default NULL,
  `queue_tableName` varchar(128) NOT NULL,
  PRIMARY KEY  (`QueueDialplanParametersID`)
);
```

Используя `func_odbc`, вы можете написать функцию, которая будет возвращать параметры диалплана, относящиеся к этой очереди:

```
[QUEUE_DETAILS]
prefix=GET
dsn=asterisk
readsql=SELECT * FROM pbx.QueueDialplanParameters
readsql+= WHERE QueueDialplanParametersID='${ARG1}'
```

Затем передайте эти параметры приложению `Queue()` по мере поступления вызовов:

```
exten => s,1,Verbose(1,Call entering queue named ${SomeValidID)
  same => n,Set(QueueParameters=${GET_QUEUE_DETAILS(SomeValidID)})
  same => n,Queue(${QueueParameters})
```
Хотя это несколько сложнее в разработке, чем просто написание соответствующего диалплана, преимущество состоит в том, что вы сможете управлять большим числом очередей с более широким набором параметров, используя диалплан, который достаточно гибок для обработки любых параметровкоторые принимает приложение очередей в Asterisk. Для чего-то большего, чем очень простая очередь, мы думаем, что вы найдете использование базы данных более удобным на которую и возможно возложить все эти усилия.

### Запись queue_log в базу данных

Наконец, мы можем хранить наш журнал `queue_log` в базе данных, что может упростить внешними приложениями извлечение сведений из системы о производительности очереди:

```
CREATE TABLE queue_log (
  id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  time char(26) default NULL,
  callid varchar(32) NOT NULL default '',
  queuename varchar(32) NOT NULL default '',
  agent varchar(32) NOT NULL default '',
  event varchar(32) NOT NULL default '',
  data1 varchar(100) NOT NULL default '',
  data2 varchar(100) NOT NULL default '',
  data3 varchar(100) NOT NULL default '',
  data4 varchar(100) NOT NULL default '',
  data5 varchar(100) NOT NULL default '',
  PRIMARY KEY (`id`)
);
```

Отредактируйте свой файл _extconfig.conf_ для ссылки на таблицу `queue_log`:

```
[settings]
queue_log => odbc,asterisk,queue_log
```

Перезагрузите Asterisk, и ваша очередь теперь будет записывать информацию в базу данных. Например, вход агента в очередь sales должна производить что-то вроде этого:

A restart of Asterisk, and your queue will now log information to the database. As an example, logging an agent into the sales queue should produce something like this:

```
mysql> select * from queue_log;
+----+----------------------------+----------------------+-----------+
| id | time                       | callid               | queuename |
+----+----------------------------+----------------------+-----------+
|  1 | 2013-01-22 15:07:49.772263 | NONE                 | NONE      |
|  2 | 2013-01-22 15:07:49.809028 | toronto-1358885269.1 | support   |
+----+----------------------------+----------------------+-----------+


+------------------+------------+-------+-------+-------+-------+-------+
| agent            | event      | data1 | data2 | data3 | data4 | data5 |
+------------------+------------+-------+-------+-------+-------+-------+
| NONE             | QUEUESTART |       |       |       |       |       |
| SIP/0000FFFF0001 | ADDMEMBER  |       |       |       |       |       |
+------------------+------------+-------+-------+-------+-------+-------+
```

Если вы разрабатываете какое-либо внешнее приложение, которое нуждается в доступе к статистике очередей, хранение данных таким образом будет намного лучше, чем использование файла _/var/log/asterisk/queue_log_.

## Вывод

В этой главе вы узнали о нескольких областях, где Asterisk может интегрироваться с реляционной базой данных. Это полезно для систем, где необходимо начать масштабирование путем кластеризации нескольких блоков Asterisk, работающих с одной и той же централизованной информацией, или когда вы хотите начать создавать внешние приложения для изменения информации, не требующие перезагрузки системы (т.е. без изменения плоских файлов).

<ol>
  <li id="sn1">На самом деле это было проблемой, с которой столкнулся один из авторов во время работы над этой книгой, и он нашел столбец <code>flag</code>, посмотрев на запись лога во время тестирования.</li>
  <li id="sn2">И если вы не знаете, что такое Дагвуд, то для этого и существует Википедия. Я не настолько стар.</li>
  <li id="sn3">Мы используем функцию SQL <code>IF()</code>, чтобы убедиться, что возвращаемое значение <code>0</code> или <code>1</code>. Это работает на MySQL 5.1 или более поздней версии. Если оно не работает в вашей установке SQL, вы также можете проверить возвращаемый результат в диалплане, используя функцию <code>IF()</code> там.</li>
  <li id="sn4">Обратите внимание, что в первом примере пользователю присваивается статус <code>1</code> и местоположение, в то время как для второго примера мы не определяем значения для этих полей.</li>
  <li id="sn5">Да, вы можете вложить функции в функции, и поэтому сделайте это все в одной строке. Мы не сделали этого, так как это сложнее отлаживать и не влияет на производительность.</li>
  <li id="sn6">Это также может представлять ненужную угрозу безопасности.</li>
  <li id="sn7">Да, вызов этого "realtime" несколько вводит в заблуждение, поскольку обновления данных не повлияют ни на что происходящее в реальном времени (пока не будет выполнена перезагрузка соответствующего модуля).</li>
  <li id="sn8"><i>pgcluster</i> по-видимому, является мертвым проектом, а Postgres-R, скорей всего, находится в зачаточном состоянии, поэтому в настоящее время не может быть хорошего решения для репликации master-master с помощью PostgreSQL.</li>
  <li id="sn9">Есть несколько учебных пособий в интернете, описывающих, как настроить репликацию с MySQL.</li>
  <li id="sn10">Вы можете увидеть различные зарегистрированные бэкенды, в зависимости от того, какую конфигурацию вы сделали с другими компонентами различных модулей CDR.</li>
  <li id="sn11">Обратите внимание, что мы создаем эту таблицу в нашей схеме <code>pbx</code>, а не в схеме <code>asterisk</code>, и это потому, что это не таблица, которая поставляется вместе с Asterisk, а та, которую мы создаем сами. Мы рекомендуем разрешить Asterisk и Alembic иметь исключительный контроль над схемой <code>asterisk</code> и использовать пользовательскую схему (например, <code>pbx</code>) для всего, что мы можем создать.</li>
</ol>

<hr>

[Глава 14. Автосекретарь](glava-14.md) | [Содержание](SUMMARY.md) | [Глава 16. Введение в интерактивное голосовое меню](glava-16.md)
