Скриптинг для бюджетной активации (часть 1)

от автора

Некоторое время назад, мне довелось поучаствовать в крупном международном проекте в составе команды активации. Суть проекта сводилась к автоматизации выполнения ряда команд на оборудовании Cisco. Разработка активационных скриптов велась на JavaScript. Главная мысль, которую я вынес из этого проекта, заключалась в том, что разработка и отладка активационных скриптов на JavaScript крайне трудоемкое занятие. Интенсивная разработка велась в течение 1 года, а только наша группа активации включала в себя около 10 разработчиков (тестеров требовалось не меньше).

В июне этого года, уже местное руководство, осчастливило меня новым активационным проектом, в котором маршрутизаторы Cisco сменились на АТС Alcatel S12 и M200. Вторым отличием этого проекта было то, что всю активационную часть предстояло разработать мне одному, с нуля, в течение полугода.

Разумеется, у меня и мысли не возникло использовать для активационных скриптов JavaScript или что-то подобное. Мне был необходим бюджетный вариант скриптинга. После некоторых размышлений, я решил хранить скрипты активации в БД Oracle, используя для хранения следующую структуру:

image

Из этой схемы видно, что части скрипта (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-файл следующего вида:

image

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

Около месяца назад, меня попросили провести ряд занятий, посвященных архитектуре нового активационного проекта (не только скриптинга, а всего проекта в целом). При разработке одной из презентаций, была нарисована следующая картинка:

image

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

Потом я подумал еще немного и решил использовать PL/SQL, а не Perl. Действительно, одним вызовом хранимой процедуры, я мог сформировать текст скрипта в CLOB, и это было ничуть не хуже чем скрипт в текстовом файле. Вот что получилось в результате:

ae_scripting.sql

 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.

Вот что получается в результате работы этой хранимки (весь скрипт целиком я не привожу):

1420.ae

[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 не задано, оно назначается автоматически, при загрузке скрипта в БД.

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

В следующей статье, я займусь реализацией этой возможности.

ссылка на оригинал статьи http://habrahabr.ru/post/162865/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *