Некоторое время назад, мне довелось поучаствовать в крупном международном проекте в составе команды активации. Суть проекта сводилась к автоматизации выполнения ряда команд на оборудовании Cisco. Разработка активационных скриптов велась на JavaScript. Главная мысль, которую я вынес из этого проекта, заключалась в том, что разработка и отладка активационных скриптов на JavaScript крайне трудоемкое занятие. Интенсивная разработка велась в течение 1 года, а только наша группа активации включала в себя около 10 разработчиков (тестеров требовалось не меньше).
В июне этого года, уже местное руководство, осчастливило меня новым активационным проектом, в котором маршрутизаторы Cisco сменились на АТС Alcatel S12 и M200. Вторым отличием этого проекта было то, что всю активационную часть предстояло разработать мне одному, с нуля, в течение полугода.
Разумеется, у меня и мысли не возникло использовать для активационных скриптов JavaScript или что-то подобное. Мне был необходим бюджетный вариант скриптинга. После некоторых размышлений, я решил хранить скрипты активации в БД Oracle, используя для хранения следующую структуру:
Из этой схемы видно, что части скрипта (AE_SCRIPT_PART), представляющие собой скрипт (AE_SCRIPT) или команду (AE_COMMAND), связываются в иерархическую структуру, посредством таблицы AE_SUBSCRIPT (первоначально AE_SCRIPT_PART просто содержала поле PARENT, ссылающееся на эту-же таблицу, но такое решение препятствовало повторному использованию фрагментов скриптов). Таблица AE_SETTING служит для привязки к скриптам и командам значений ряда настроек, определенных AE_SETTING_TYPE, таких как условие выполнения (if_condition) или переменная цикла (for_each).
Первоначально была предусмотрена возможность создания шаблонов команд (AE_COMMAND_TEMPLATE), связанных с набором заранее определенных значений настроек, но эта возможность оказалась избыточной и, в настоящее время, почти не используется. Фактически, эта схема данных позволяет хранить AST.
Чтобы не заблудиться во всем этом безобразии, параллельно наполнению скрипта в БД, велся Excel-файл следующего вида:
Это давало возможность вести разработку, но передать эту работу (или часть работ) другому сотруднику, в таком виде, разумеется, не представлялось возможным. Конечно, я начал задумываться о более дружественном скриптинге.
Около месяца назад, меня попросили провести ряд занятий, посвященных архитектуре нового активационного проекта (не только скриптинга, а всего проекта в целом). При разработке одной из презентаций, была нарисована следующая картинка:
Текст в нижней стрелочке и послужил прототипом разрабатываемого скриптового языка. Я просто подумал, что было бы неплохо, используя Perl, например, вынуть описание скрипта из БД и представить в компактном, читабельном виде, для того чтобы проверить его на отсутствие ошибок.
Потом я подумал еще немного и решил использовать PL/SQL, а не Perl. Действительно, одним вызовом хранимой процедуры, я мог сформировать текст скрипта в CLOB, и это было ничуть не хуже чем скрипт в текстовом файле. Вот что получилось в результате:
create or replace package body ae_scripting as
g_if_setting constant varchar2(100) default 'if_condition';
g_foreach_setting constant varchar2(100) default 'foreach_var';
g_target_setting constant varchar2(100) default 'target_platform';
g_if_statement constant varchar2(100) default 'if';
g_foreach_statement constant varchar2(100) default 'foreach';
procedure extract(p_id in number) as
cursor c_cmd is
select d.id, d.nm, d.lv, d.type_id, p.name platform, t0.value target,
f.direction_id direction
from ( select level lv, sys_connect_by_path(to_char(c.nm, '00000'), '/') pt,
sys_connect_by_path(c.id, '.') nm, c.id id, c.type_id
from ( select a.id id, a.type_id type_id, b.parent_id parent_id,
b.order_num nm
from ae_script_part a, ae_subscript b
where b.child_id(+) = a.id ) c
start with c.id = p_id
connect by prior c.id = c.parent_id
order by pt ) d
left join ae_script e on (e.id = d.id)
left join ae_command f on (f.id = d.id)
left join ae_platform p on (p.id = e.platform_id)
left join ae_setting t0 on (t0.object_id = d.id and
t0.setting_type_id = 1000001);
r_cmd c_cmd%rowtype;
cursor c_par(p_cmd_id number) is
select t.name name, s.value value
from ae_script_part a
inner join ae_setting s on (s.object_id = a.id)
inner join ae_setting_type t on (t.id = s.setting_type_id)
where a.id = p_cmd_id
union all
select t.name, s.value
from ae_script_part a
inner join ae_command c on (c.id = a.id)
inner join ae_setting s on (s.object_id = c.template_id)
inner join ae_setting_type t on (t.id = s.setting_type_id)
where a.id = p_cmd_id;
r_par c_par%rowtype;
l_str varchar2(1000) default null;
l_lob CLOB;
l_lvl number default 0;
l_plv number default 0;
l_cnt number default null;
l_stt varchar2(50) default null;
begin
delete from ae_script_src where id = p_id;
insert into ae_script_src(id, text) values (p_id, empty_clob());
select text into l_lob from ae_script_src where id = p_id;
dbms_lob.open(l_lob, dbms_lob.lob_readwrite);
open c_cmd;
loop
fetch c_cmd into r_cmd;
exit when c_cmd%notfound;
while r_cmd.lv <= l_plv loop
l_str := ' ' || lpad('}', 2 * l_lvl) || chr(13) || chr(10);
dbms_lob.writeappend(l_lob, length(l_str), l_str);
l_plv := l_plv - 1;
l_lvl := l_lvl - 1;
end loop;
l_str := '[' || trim(to_char(r_cmd.id, '000000')) || '] ';
if not r_cmd.direction is null then
if r_cmd.direction = '1' then
l_str := l_str || '<';
else
l_str := l_str || '>';
end if;
else
l_str := l_str || ' ';
end if;
l_str := l_str || lpad(' ', 2 * r_cmd.lv);
if not r_cmd.platform is null then
l_str := l_str || 'platform:' || r_cmd.platform || '; ';
end if;
if not r_cmd.target is null then
l_str := l_str || 'target:' || r_cmd.target || '; ';
end if;
open c_par(r_cmd.id);
l_stt := null;
loop
fetch c_par into r_par;
exit when c_par%notfound;
if l_stt is null and r_par.name = g_if_setting then
l_str := l_str || g_if_statement || ' (' || r_par.value || ') { ';
l_stt := r_par.name;
end if;
if l_stt is null and r_par.name = g_foreach_setting then
l_str := l_str || g_foreach_statement || ' (' ||
r_par.value || ') { ';
l_stt := r_par.name;
end if;
end loop;
close c_par;
open c_par(r_cmd.id);
loop
fetch c_par into r_par;
exit when c_par%notfound;
if l_stt is null or l_stt <> r_par.name then
if r_par.name <> g_target_setting then
l_str := l_str || r_par.name || ':' || r_par.value || '; ';
end if;
end if;
end loop;
close c_par;
select count(*) into l_cnt
from ae_subscript
where parent_id = r_cmd.id;
if r_cmd.type_id = 1 and l_cnt > 0 then
if l_stt is null then
l_str := l_str || '{';
end if;
l_lvl := l_lvl + 1;
l_plv := r_cmd.lv;
else
if not l_stt is null then
l_str := l_str || ' }';
end if;
l_plv := r_cmd.lv - 1;
end if;
l_str := l_str || chr(13) || chr(10);
dbms_lob.writeappend(l_lob, length(l_str), l_str);
end loop;
close c_cmd;
while l_lvl > 0 loop
l_str := ' ' || lpad('}', 2 * l_lvl) || chr(13) || chr(10);
dbms_lob.writeappend(l_lob, length(l_str), l_str);
l_lvl := l_lvl - 1;
end loop;
dbms_lob.close(l_lob);
commit;
exception
when others then
if c_cmd%isopen then close c_cmd; end if;
if c_par%isopen then close c_par; end if;
if dbms_lob.isopen(l_lob) = 1 then dbms_lob.close(l_lob); end if;
rollback;
raise;
end;
end ae_scripting;
/
После построения дерева скрипта connect by запросом, остальное почти тривиально. Некоторые сложности были связаны только с генерацией правильной последовательности процедурных скобок '{' и '}' определяющих вложенность скриптов. Для настроек if_condition и foreach_var генерируются более привычные формы операторов if и foreach.
Вот что получается в результате работы этой хранимки (весь скрипт целиком я не привожу):
[001420] target:ats.type; foreach (params) {
[003101] platform:S-12; if (dou_off.dou = 'REDIRECT_NOANSWER') {
[031010] < text:MODIFY-SUBSCR:DN=K'%s,CFWD=DEACT&CFWDNOR.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003111] platform:S-12; if (dou_off.dou = 'REDIRECT_BUSY') {
[031110] < text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDBSUB.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003121] platform:S-12; if (dou_off.dou = 'REDIRECT_AUTOINF') {
[031210] < text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDFIXA.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003131] platform:S-12; if (dou_off.dou = 'REDIRECT') {
[031310] < text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDUVAR.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003071] platform:S-12; if (dou_off.dou = 'SET_ALARM_CLOCK') {
[030710] < text:MODIFY-SUBSCR:DN=K'%s,ALMCALL=DEACT.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001009] var_list:is_alarm_param = 1;
}
}
Каждая строка скрипта (за исключением закрывающихся процедурных скобок) определяет скрипт или команду. Команда определяется символами '<' и '>', определяющими направление передачи данных (на оборудование и с него). Настройки определяются следующей последовательностью:
<Имя настройки>:<Значение>;
Символ ';' является разделителем настроек и не должен использоваться внутри значения настройки (вообще говоря, в командах S12 этот символ используется в качестве разделителя команд, но я завершаю команды символом '.' и необходимости использования ';' внутри команды не возникает). В любом случае, добавить экранирование служебных символов внутри значений совсем не сложно.
Важной, но не обязательной частью скрипта являются числа в квадратных скобках. Это рекомендуемые значения ID для размещения скрипта или команды в БД. Задав одинаковое значение ID для команд или скриптов, можно добиться повторного использования фрагмента скрипта (при условии того, что помеченные фрагменты действительно идентичны), разместив этот фрагмент в БД однократно. Если значение ID не задано, оно назначается автоматически, при загрузке скрипта в БД.
Итак, я научился извлекать скрипты из описания в БД и представлять их в компактной и наглядной форме, но апетит приходит во время еды, и хочется уже большего. Действительно, как здорово было-бы иметь возможность исправить скрипт в текстовой форме и загрузить обратно в БД.
В следующей статье, я займусь реализацией этой возможности.
Автор: GlukKazan