В ту же реку
Относительно недавно я написал себе шпаргалку по настройке FreeSWITCH. Описанный там процесс настройки привел к работоспособной в тестовых условиях конфигурации. Тест был необходим для составления предварительного представления о том, с чем придется иметь дело после переезда организации и запуске телефонии в продакшн. Однако, когда переезд состоялся и началось подключение в рабочем режиме, то первое же включение показало неработоспособность конфигурации: перестали ходить внутренние вызовы.
Это стало для меня полнейшим сюрпризом, поскольку с момента финальной настройки и проверки работоспособности, по мотивам которой была написана шпаргалка, по момент включения в рабочем режиме никаких изменений в конфиг не вносилось. Были лишь массово добавлены внутренние номера и маршруты для входящих и исходящих вызовов для тех сотрудников, за кем были закреплены прямые городские номера (порядка 60 с хвостиком номеров).
Был проведен дебаг, выявлен косяк, и все заработало. Однако, осталось ощущение костыля. Описывать его не стану, поскольку пребываю в уверенности, что примененное решение не верное, хоть и привело к искомому результату. Кроме того, выяснились нюансы: при исходящих вызовах изнутри наружу определялся только тот номер, что был указан в настройке SIP-транка в поле default_provider_username:
<X-PRE-PROCESS cmd="set" data="default_provider_username=3435555555"/>
а не тот, что указан в конфигурации абонентского номера:
<variable name="outbound_caller_id_name" value="3435555566"/>
Техподдержка провайдера сообщила, что все вызовы, прилетающие к ним от нас, в поле From имеют именно номер 3435555555, то есть косяк на моей стороне. Плюс ко всему, я вдруг совершенно завис с задачей переадресации вызовов. А вишенкой на торте стал вынос мозга аппаратами Ericsson Dialog 4422, отказавшимися выполнять трансфер вызова, и аппаратами Cisco 7945g, решившими, что их предел длительности соединения составляет 90-100 секунд при отсутствии малейшего намека на подобную настройку в конфиге. В то же время аппараты Yealink T21 E2 работали полностью без нареканий.
На этом этапе я осознал, что достиг предела своей компетенции в области телефонии и взял тайм-аут на то, чтобы все в голове утряслось и уложилось. Этому решению так же очень могуче способствовало общее утомление после совершенно диких двух рабочих недель без выходных и с ненормированным рабочим графиком, которые последовали сразу после заезда на новое место размещения организации.
FusionPBX
Не смотря на отсутствие у меня симпатий к графическим интерфейсам там, где правит бал консоль и текстовые конфиги, я все же стал смотреть в сторону решения с веб-мордой, именуемого FusionPBX. Первой причиной такой измены собственным принципам стало желание видеть весь объем настроек по каждому функциональному элементу, собранных в одном месте в виде работоспособной «из коробки» конфигурации. Именно такую возможность дает графический интерфейс. Дополнительным бонусом продуманного графического интерфейса является наглядное представление взаимосвязей между модулями и функциями. Для новичка (лично для меня) меньший уровень абстракции с конкретным способом реализации способствует более быстрому обучению и приходу к понимаю того, как эта штука работает. Второй причиной стал www.pbxforums.com, на который я попадал по ссылке через одну при поиске информации по FreeSWITCH, и попадал по иронии судьбы именно на скриншоты страниц настроек FusionPBX.
FusionPBX это FreeSWITCH с веб-мордой и с настройками, хранящимися в базе данных. Скрипт автоматической установки выполняет установку и FreeSWITCH'а, и Nginx'а, и PostgreSQL, и, собственно, веб-интерфейса самого FusionPBX. Останавливаться на этом моменте не стану, все без запинок ставится по инструкции из документации. Ставил все на рекомендуемую разработчиками 64-битную Debian 8.
Импорт абонентских номеров
Здесь не будет рассматриваться процесс настройки абонентских номеров и входящих маршрутов. Этот процесс описан в официальной документации.
Вместо него будет описана процедура импорта всего скопом. Описаний, мануалов и советов по выполнению данной процедуры мною найдено не было.
По окончании установки включаем автоматический вход в Adminer (аналог phpMyAdmin):
Advanced>Default settings:
auto_login
Value: true
Enabled: true
После изменения значений на текущей странице нажимаем Save, на странице настроек по умолчанию Reload.Переходим в Adminer: Advanced>Adminer.
Интерес для нас представляют следующие таблицы:
v_extensions — абонентские номера.
v_destinations — маршруты для входящих вызовов на городские номера, закрепленные за внутренними абонентскими номерами.
v_dialplans — справочник диалпланов.
v_dialplan_details — настройки диалпланов входящих вызовов.
v_voicemails — настройки голосовой почты.
Формулировка задачи была следующей: выгрузить из AD ФИО сотрудников и их номера внутренних телефонов, сохранить выгрузку в CSV-файл и импортировать его в БД в таблицу абонентских номеров и настроек голосовой почты (голосовая почта должна быть отключена).
Используя справочник соответствия городских номеров внутренним, создать CSV-файлы для импорта в таблицы с маршрутами и диалпланами входящих вызовов.
Я не стану подробно рассматривать эту задачу, просто спрячу готовые скрипты под спойлер.
Внимание!
Предлагаемые скрипты вы используете на свой страх и риск, автор не несет ответственности за их неправильное использование или неожиданные побочные эффекты их правильного использования.
.
- Присвойте переменной $nums значения, соответствующие вашим номерам.
- Перед использованием скриптов необходимо везде заменить UUID домена на значение, присвоенное домену при установке (поле domain_uuid).
- Так же необходимо заменить IP-адрес домена (172.18.253.1) на ваш.
- Не забудьте откорректировать значение ключа -SearchBase, указав свою область выборки вместо «OU=Ekaterinburg,DC=dc,DC=domain,DC=local»
- UUID приложения Voicemail (поле app_uuid) так же заменить на UUID, присвоенный при установке.
- Значения UUID'ов можно посмотреть, например, в таблице v_dialplans.
- Всем абонентским номерам будет присвоен пароль для регистрации «12345», пароль на голосовую почту и прочие сервисы — совпадающий с абонентским номером.
- Скрипт дописывает файлы построчно! Поэтому не забывайте удалять файлы перед каждым запуском скрипта или очищать их содержимое!
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
$nums=@{"1111"="5555555";"1112"="5555566"}
[System.IO.File]::AppendAllText("d:\v_extensions.csv", "extension_uuid;domain_uuid;extension;number_alias;password;accountcode;effective_caller_id_name;effective_caller_id_number;outbound_caller_id_name;outbound_caller_id_number;emergency_caller_id_name;emergency_caller_id_number;directory_full_name;directory_visible;directory_exten_visible;limit_max;limit_destination;missed_call_app;missed_call_data;user_context;toll_allow;call_timeout;call_group;call_screen_enabled;user_record;hold_music;auth_acl;cidr;sip_force_contact;nibble_account;sip_force_expires;mwi_account;sip_bypass_media;unique_id;dial_string;dial_user;dial_domain;do_not_disturb;forward_all_destination;forward_all_enabled;forward_busy_destination;forward_busy_enabled;forward_no_answer_destination;forward_no_answer_enabled;follow_me_uuid;enabled;description;forward_caller_id_uuid;absolute_codec_string;forward_user_not_registered_destination;forward_user_not_registered_enabled;force_ping`r`n", $Utf8NoBomEncoding)
[System.IO.File]::AppendAllText("d:\v_voicemails.csv", "domain_uuid;voicemail_uuid;voicemail_id;voicemail_password;greeting_id;voicemail_alternate_greet_id;voicemail_mail_to;voicemail_sms_to;voicemail_attach_file;voicemail_file;voicemail_local_after_email;voicemail_enabled;voicemail_description;voicemail_name_base64`r`n", $Utf8NoBomEncoding)
Get-ADUser -Filter * -SearchBase "OU=Ekaterinburg,DC=dc,DC=domain,DC=local" -Properties Telephonenumber,sn,initials,cn|%{
if(-not $_.Telephonenumber -eq ""){
if($nums.Get_Item($_.Telephonenumber) -eq $null)
{$outn = "5555555"}
else
{$outn = $nums.Get_Item($_.Telephonenumber)}
$extension_uuid = (New-Guid).Tostring()
$domain_uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff" ## Заменить!!!
$extension = $_.Telephonenumber
$number_alias = ""
$password = "12345"
$accountcode = "172.18.253.1"
$effective_caller_id_name = $_.sn + " " + $_.initials
$effective_caller_id_number = $extension
$outbound_caller_id_name = $outn
$outbound_caller_id_number = $outn
$emergency_caller_id_name = $effective_caller_id_name
$emergency_caller_id_number = $extension
$directory_full_name = $_.cn
$directory_visible = "true"
$directory_exten_visible = "true"
$limit_max = "1"
$limit_destination = "error/user_busy"
$missed_call_app = ""
$missed_call_data = ""
$user_context = "172.18.253.1"
$toll_allow = "domestic,international,local"
$call_timeout = "30"
$call_group = ""
$call_screen_enabled = "false"
$user_record = ""
$hold_music = "local_stream://default"
$auth_acl = ""
$cidr = ""
$sip_force_contact = ""
$nibble_account = ""
$sip_force_expires = "3600"
$mwi_account = ""
$sip_bypass_media = ""
$unique_id = ""
$dial_string = ""
$dial_user = ""
$dial_domain = ""
$do_not_disturb = ""
$forward_all_destination = ""
$forward_all_enabled = ""
$forward_busy_destination = ""
$forward_busy_enabled = ""
$forward_no_answer_destination = ""
$forward_no_answer_enabled = ""
$follow_me_uuid = ""
$enabled = "true"
$description = $_.sn + " " + $_.initials
$forward_caller_id_uuid = ""
$absolute_codec_string = ""
$forward_user_not_registered_destination = ""
$forward_user_not_registered_enabled = ""
$force_ping = ""
$csv="$extension_uuid;$domain_uuid;$extension;$number_alias;$password;$accountcode;$effective_caller_id_name;$effective_caller_id_number;$outbound_caller_id_name;$outbound_caller_id_number;$emergency_caller_id_name;$emergency_caller_id_number;$directory_full_name;$directory_visible;$directory_exten_visible;$limit_max;$limit_destination;$missed_call_app;$missed_call_data;$user_context;`"$toll_allow`";$call_timeout;$call_group;$call_screen_enabled;$user_record;$hold_music;$auth_acl;$cidr;$sip_force_contact;$nibble_account;$sip_force_expires;$mwi_account;$sip_bypass_media;$unique_id;$dial_string;$dial_user;$dial_domain;$do_not_disturb;$forward_all_destination;$forward_all_enabled;$forward_busy_destination;$forward_busy_enabled;$forward_no_answer_destination;$forward_no_answer_enabled;$follow_me_uuid;$enabled;$description;$forward_caller_id_uuid;$absolute_codec_string;$forward_user_not_registered_destination;$forward_user_not_registered_enabled;`"$force_ping`"`r`n"
[System.IO.File]::AppendAllText("d:\v_extensions.csv", $csv, $Utf8NoBomEncoding)
$voicemail_uuid = (New-Guid).Tostring()
$voicemail_id = $extension
$voicemail_password = $extension
$greeting_id
$voicemail_alternate_greet_id
$voicemail_mail_to = ""
$voicemail_sms_to
$voicemail_attach_file
$voicemail_file = ""
$voicemail_local_after_email = "true"
$voicemail_enabled = "false"
$voicemail_description = $description
$voicemail_name_base64
[System.IO.File]::AppendAllText("d:\v_voicemails.csv", "$domain_uuid;$voicemail_uuid;$voicemail_id;$voicemail_password;$greeting_id;$voicemail_alternate_greet_id;$voicemail_mail_to;$voicemail_sms_to;$voicemail_attach_file;$voicemail_file;$voicemail_local_after_email;$voicemail_enabled;$voicemail_description;$voicemail_name_base64`r`n", $Utf8NoBomEncoding)}}
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::AppendAllText("d:\v_destinations.csv", "domain_uuid;destination_uuid;dialplan_uuid;fax_uuid;destination_type;destination_number;destination_number_regex;destination_caller_id_name;destination_caller_id_number;destination_cid_name_prefix;destination_context;destination_app;destination_data;destination_enabled;destination_description;destination_accountcode`r`n", $Utf8NoBomEncoding)
[System.IO.File]::AppendAllText("d:\v_dialplans.csv", "domain_uuid;dialplan_uuid;app_uuid;dialplan_context;dialplan_name;dialplan_number;dialplan_continue;dialplan_order;dialplan_enabled;dialplan_description`r`n", $Utf8NoBomEncoding)
[System.IO.File]::AppendAllText("d:\v_dialplan_details.csv", "domain_uuid;dialplan_uuid;dialplan_detail_uuid;dialplan_detail_tag;dialplan_detail_type;dialplan_detail_data;dialplan_detail_break;dialplan_detail_inline;dialplan_detail_group;dialplan_detail_order`r`n", $Utf8NoBomEncoding)
$nums="1111=5555555;1112=5555566"
$nums.Split(";")|%{
$innum = $_.Split("=")[0]
$outnum = $_.Split("=")[1]
$domain_uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff" ## Заменить!!!
$destination_uuid = (New-Guid).Tostring()
$dialplan_uuid = (New-Guid).Tostring()
$fax_uuid
$destination_type = "inbound"
$destination_number = "343$outnum"
$destination_number_regex = "^(343$outnum)$"
$destination_caller_id_name
$destination_caller_id_number
$destination_cid_name_prefix
$destination_context = "public"
$destination_app
$destination_data
$destination_enabled = "true"
$destination_description = "$outnum-$innum"
$destination_accountcode
[System.IO.File]::AppendAllText("d:\v_destinations.csv", "$domain_uuid;$destination_uuid;$dialplan_uuid;$fax_uuid;$destination_type;$destination_number;$destination_number_regex;$destination_caller_id_name;$destination_caller_id_number;$destination_cid_name_prefix;$destination_context;$destination_app;$destination_data;$destination_enabled;$destination_description;$destination_accountcode`r`n", $Utf8NoBomEncoding)
$app_uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff" ## Заменить!!!
$dialplan_context = "public"
$dialplan_name = $destination_number
$dialplan_number = $destination_number
$dialplan_continue = "false"
$dialplan_order = "100"
$dialplan_enabled = "true"
$dialplan_description = $destination_description
[System.IO.File]::AppendAllText("d:\v_dialplans.csv", "$domain_uuid;$dialplan_uuid;$app_uuid;$dialplan_context;$dialplan_name;$dialplan_number;$dialplan_continue;$dialplan_order;$dialplan_enabled;$dialplan_description`r`n", $Utf8NoBomEncoding)
$dialplan_detail_break
$dialplan_detail_inline
$dialplan_detail_group
$dialplan_detail_uuid = (New-Guid).Tostring()
$dialplan_detail_tag = "condition"
$dialplan_detail_type = "destination_number"
$dialplan_detail_data = "^(343$outnum)$"
$dialplan_detail_order = 20
[System.IO.File]::AppendAllText("d:\v_dialplan_details.csv", "$domain_uuid;$dialplan_uuid;$dialplan_detail_uuid;$dialplan_detail_tag;$dialplan_detail_type;$dialplan_detail_data;$dialplan_detail_break;$dialplan_detail_inline;$dialplan_detail_group;$dialplan_detail_order`r`n", $Utf8NoBomEncoding)
$dialplan_detail_uuid = (New-Guid).Tostring()
$dialplan_detail_tag = "action"
$dialplan_detail_type = "transfer"
$dialplan_detail_data = "$innum XML 172.18.253.1"
$dialplan_detail_order = 30
[System.IO.File]::AppendAllText("d:\v_dialplan_details.csv", "$domain_uuid;$dialplan_uuid;$dialplan_detail_uuid;$dialplan_detail_tag;$dialplan_detail_type;$dialplan_detail_data;$dialplan_detail_break;$dialplan_detail_inline;$dialplan_detail_group;$dialplan_detail_order`r`n", $Utf8NoBomEncoding)
}
Проверка связи на рандомно выбранные номера показала работоспособность импорта.
Настройка шлюза
Accounts>Gateways
Gateway: 172.16.253.3
Username: 3435555555
Password: not-used
From User: 3435555555
From Domain: 172.16.253.3
Proxy: 172.16.253.3
Register: False
Caller ID In From: True
Обратите внимание!В документации по FusionPBX недвусмысленно указывается, что при выполнении настроек поля, выделенные жирным текстом, обязательны для заполнения.Настройка ACL
Однако я, по непонятной мне причине, жирность поля Proxy не углядел и значение ему не выставил. В итоге получил работающие входящие внешние вызовы, но не работающие исходящие наружу. Командаsofia status gateway ffffffff-ffff-ffff-ffff-ffffffffffff
не показывала аномалий настройки и даже показывала назначенное значение поля Proxy, соответствующее значению Gateway. Точно такой же вывод команды при точно таких же настройках демонстрировал «голый» FreeSWITCH в предыдущей инсталляции, и при этом совершенно беспроблемно позволял совершать исходящие вызовы наружу.
FusionPBX же заработал только после явного указания значения Proxy.
*ffffffff-ffff-ffff-ffff-ffffffffffff
— UUID шлюза
Выполнил настройки в соответствии со шпаргалкой и тут же получил сломавшиеся внутренние вызовы. Логи показывали, что аппараты почему-то оказались в контексте external, соответственно, обрабатывались «не своим» диалпаном, от чего вызов завершался ошибкой ROUTE_NOT_FOUND.
Как выяснилось, настройка ACL была выполнена неправильно!
Важно!
ACL-списки только для сетей и доменов провайдеров.
Ваших собственных сетей и доменов в них быть не должно.
Список domains должен быть по умолчанию deny.
Сами правила должны быть разрешающими и в них должен быть прописан IP-адрес шлюза провайдера с маской /32, поле domain заполнять не нужно.
Итак, выполняем настройку ACL: Advanced>Access Controls>domains. Удаляем существующие правила, создаем новое:
Type: allow
CIDR: 172.16.253.3/32
Domain:
Description: default SIP-trunk
По окончании жмем Save, далее чтобы новые ACL вошли в силу: Status>Sip Status и жмем Reload ACL.
Системные переменные
Advanced>Default Settings
Здесь мы укажем выданный нам провайдером внешний IP-адрес, который мы использовали при настройке 1:1 NAT в шпаргалке, укажем телефонный код региона, язык и голос для голосовых ответов, тип гудка.
Раздел Defaults:
default_areacode: 343
default_language: ru
default_dialect: RU
default_voice: elena
ringback: $${ru-ring}
transfer_ringback: $${ru-ring}
Раздел IP Addressexternal_rtp_ip: 172.16.160.154
external_sip_ip: 172.16.160.154
Раздел SIP Profile: Internalinternal_auth_calls: true
Собственно говоря, именно эта переменная в значении true отвечает за считывание настроек абонентского номера и передачу из него значений ${outbound_caller_id_number} и ${outbound_caller_id_name}. Чтобы эта переменная имела силу, необходимо, чтобы была отключена авторизация внутренних абонентских номеров по ACL. По умолчанию, из коробки, это сделано и так: ACL-авторизация отсутствует, вместо нее используется Digest (по абонентскому номеру и паролю): internal_auth_calls: true
.Важно!Исходящие маршруты
Чтобы корректно определялись прямые городские номера, присвоенные внутренним в настройках через поля Outbound Caller ID Name и Outbound Caller ID Number, необходимо выполнение трех условий:
- Отсутствие ACL-авторизации внутренних абонентов
- Включенная Digest-авторизация в настройках SIP-профиля:
internal_auth_calls: true
- Наличие в настройках шлюза:
Caller ID In From: True
Dialplan>Outbound Routes
Пожалуй, это единственный пункт настроек, не подвергшийся переосмыслению.
Подробно разбирать его не стану. Отмечу лишь, что были использованы следующие регулярные выражения для различных направлений:
- Внутригород:
^(\d{7})$
(набор прямого городского 7-значного номера без всяких префиксов в виде нулей, девяток и прочего). - Внутригород с кодом города:
^(8343\d{7})$
(набор городского 7-значного номера с префиксом 8343). - Сотовые:
^(89\d{9})$
(звонок на сотовый с префиксом 8, что является стандартом де-факто) - Межгород:
^(8\d{10})$
(междугородний звонок, так же привычные: 8, код населенного пункта, номер абонента) - Международный:
^(810\d+)$
(стандартный же префикс 810, далее код страны, код территории, номер абонента).
Для всех маршрутов было отредактированы два тега action типа set:
effective_caller_id_name=${default_areacode}${outbound_caller_id_name}
effective_caller_id_number=${default_areacode}${outbound_caller_id_number}
таким образом, чтобы передаваемый оператору номер вызывающего абонента включал в себя код города.Лечим сброс вызова через 90-100 секунд на аппаратах Cisco
Как было отмечено выше, сюрпризом стал обрыв установленного соединения через 90-100 секунд на всех аппаратах Cisco 7945g. Подкручивание всех таймеров с более или менее релевантным названием переменной в конфиге аппаратов результата не дало. Курение логов в консоли FreeSWITCH выявило Session Expire.
Гуглинг, кроме матов в сторону нежелания аппаратов Cisco нормально работать хоть с кем-то, кроме Call Manager'а, выявил, что такое поведение вполне может быть вылечено отключением переменной
aggressive-nat-detection
.Advanced>SIP Profile
aggressive-nat-detection
Value: true
Enabled: False
Русификация голосового откликаНам потребуются файлы озвучки, созданные альтруистичными профессионалами.
Качаем:
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-48000-1.0.51.tar.gz
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-32000-1.0.51.tar.gz
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-16000-1.0.51.tar.gz
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-8000-1.0.51.tar.gz
Каждый из архивов содержит готовую структуру каталогов. Каждый из архивов распаковываем в /usr/share/freeswitch/sound/
Поскольку ранее мы уже выполнили настройку значений по умолчанию, с этого момента файлы русской озвучки подхватятся и начнут воспроизводиться без дополнительных движений. Единственное, что вам, возможно, придется сделать (мне пришлось), так это во всех четырех папках ru/RU/elena/voicemail/_bitrate_/ переименовать файл vm-not_available_no_voicemail.wav и дать ему новое имя vm-no_answer_no_vm.wav. Только после этой манипуляции я получил голосовой отклик на событие недоступности вызываемого абонента.
P.S.: Как и предыдущая часть, данный текст был написан исключительно с целью документирования возникающих сложностей и путей их решения. Несмотря на то, что текст так же освещает быстрый старт с нуля все того же FreeSWITCH'а, пусть и с «графическим лицом», считаю, что текст самодостаточный и является неким форком, и имеет право на самостоятельную жизнь. Предыдущая часть так же сохраняет некоторую ценность благодаря описанной настройке сетевого оборудования. Некорректные настройки в том тексте будут исправлены и приведены к тем, что используются в данной статье.
Комментарии (5)
Ovoshlook
11.04.2018 06:10Справедливости ради надо сказать что каждый начинал так свою историю покорения SIP. Сталкивался с теми же проблемами, наступал на те же грабли.
Если резюмировать вашу статью, то можно сделать выводы, что основная масса проблем идет от незнания как работает SIP, а не от наличия или отсутствия WEB интерфейса управления. Но это дело наживное.
P.S. Борьба SIP с NAT-ом перестает быть борьбой ровно после того как придет понимание как SIP работает с NAT.
mihmig
11.04.2018 14:05А где можно ознакомиться с данным знанием?
Ovoshlook
12.04.2018 06:15так как понимание проблемы лежит на уровне SIP в случае сигнализации и SDP в случае медиа:
я бы предложил начать с
RFC3261 чтобы понять какие поля ответственны за маршрутизацию траффика
и продолжить
RFC6314 — тут practices по NAT
Ну и остальное с опытом придет непременно)
Если у меня дойдут руки, то я все же напишу статью по этому поводу, так как давно хотел.
Но не факт что это будет очень скоро.
zeronice
Когда то FusionPBX генерировала конфиги из того что она хранит в базе и была возможность посмотреть на то, что получалось на выходе. Было удобство внесения изменений и контроль за конфигурацией. В последних версиях конфигурация вливается в Freeswitch "на лету" и не всегда понятна логика сгенерированного конфига(ну или ее долго выцарапывать из консоли). Дебаг такого решения то еще занятие по сравнению с человекочитаемым XML.
LazyFao Автор
Есть доля истины в ваших словах). Но лично для меня так сложилось, что ковырнув конфиг, раскиданный по таблицам, именованным по выполняемому функционалу, картинка в голове сложилась быстрее и воспринимается более слитной. При этом и понимание сделанных ранее конфигов в XML улучшилось, нашел косяки. И в целом, база данных лучше располагает к массовым правкам благодаря SQL-запросам. Мне пока что нравится больше.
И коллегам, которые на подхвате, и которые по сравнению со мной много дальше от консоли и XML, веб-морда заходит лучше. В свете необходимости делегировать им админские функции это несомненный плюс.