Программирование в RouterOS
Программирование в RouterOSПисать скрипты приходится не часто. И порой бывает подзабываешь че-каво, особенно после СИ-образных языков. Так что буду складывать в этом разделе особенности программирования под наш любимый роутер.
Mikrotik, вторые шаги
Mikrotik, вторые шагиПредисловие. Это можно не читать ))
Есть много хороших и не очень руководств по языку скриптов Mikrotik, но подробного описания как работать с функционалом(объектами) я не нашел. Пришлось собирать информацию по крупицам, разбираться по примерам. Как известно, что бы закрепить и систематизировать полученные знания, полезно их рассказать другим. Поэтому, рискну изложить то, что понял сам из разрозненных источников.
На первый взгляд может показаться, что очень много написанного не имеет отношения к написанию скриптов. Но это только на первый взгляд ;)
Подготовка
Хоть я и старался максимально подробно изложить материал, но перед началом работы имеет смысл ознакомиться с основами работы в терминале Mikrotik. Это и будут первые шаги. Вот достойный материал на эту тему: http://mikrotik.vetriks.ru/wiki/%D0%94%D0%BB%D1%8F_%D0%BD%D0%B0%D1%87%D…
Поехали
Для примера будем работать с DHCP сервером.
Открываем терминал, пункт меню «New Terminal» и вводим команду: /ip dhcp-server
и жмем кнопку табуляции(далее <TAB>). Получим такой вывод:
Здесь мы видим доступные для продолжения ввода команды значения. То что подсвечено «синим» цветом, это подразделы. В WinBox их можно увидеть в виде вкладок. Нас же, более всего интересуют «фиолетовые» команды. Пробуем набирать print и <ENTER>. На выводе получим список всех(в примере один) настроенных DHCP серверов:
Кстати, после print то же можно нажать <TAB>. Поиграйтесь с разными параметрами.
Усложняем задачу. Идем на вкладку Leases. В этом примере она выглядит так:
Отключим(не удалим(remove), а именно отключим(disable)) резервирование IP адреса для хоста, у которого текущий адрес 192.168.254.21. На скриншоте мы видим результат - команды, их вывод и отключенная запись резервирования. Разберем подробно.
Вводим: /ip dhcp-server lease <TAB>
и наблюдаем список команд. По смыслу понимаем что нам подходит команда disable, но мы сделаем вид что ее нет, и в академических целях пойдем более сложным путем. Так же видна некая команда set, вероятно она устанавливает какие-то параметры. Посмотрим что она может.
Продолжаем вводить: set <TAB>. Теперь мы видим список «зеленых» параметров, которые мы можем использовать в команде set. Обратите внимание, что параметр numbers выделен жирным цветом — он обязательный. Следуя логике можно предположить, что это некий идентификатор(номер), однозначно указывающий на изменяемый командой set объект.
Так где же нам узнать этот номер? Например через команду print, вводим:
/ip dhcp-server lease print
Следует отметить, что номера выводимые командой print не постоянны, и могут измениться при следующей команде print.
Пробуем ввести: /ip dhcp-server lease set disabled=<TAB>
и видим какие значения может принимать параметр, дописываем yes и <ENTER>. Упс, что за вопрос numbers? Да-да, тот самый номер, из первой колонки таблицы, команды print. В нашем случае вводим: 7<ENTER>.
Но все это можно сделать одной командой:
/ip dhcp-server lease set 7 disabled=yes
Забегая вперед, замечу, что при попытке получить значение disabled, командой:
:put [/ip dhcp-server lease get 7 disabled]
вы получите булево значение true или false. Это важно при использовании в скриптах, такая особенность вас может поджидать в самом неожиданном месте. О_о, а откуда взялся get, его ведь не было в списке доступных команд? Об этом то-же позже...
Чет какая-то фигня заметит пытливый читатель )) Верно, фигня. Теперь выполним задачу одной командой. Если помните, среди доступных команд есть find ее-то мы и будем использовать.
Обратите внимание на эти команды:
В первой строке: От корня иерархии объектов микротика мы двигаемся в раздел lease DHCP-сервера, где командой set изменяем значение параметра disabled у 7-го объекта.
Во второй и третьей строке проделывается то-же самое, только немного иначе. Сначала мы переходим в контекст - /ip dhcp-server lease, а затем в этом контексте выполняем команду с параметрами: set 7 disabled=yes
Теперь в нашей команде /ip dhcp-server lease set 7 disabled=yes сотрем цифру 7 и напишем [ find ]. После слова find нажмем <TAB> и увидим список параметров по которым мы можем выполнить поиск в списке объектов выделенных адресов.
Дописываем наш find, должно получиться так:
/ip dhcp-server lease set [find address=192.168.254.21] disabled=yes
Если кто заметил, в тексте IP-адрес экранирован кавычками, а на скриншоте нет. Можно использовать кавычки, можно не использовать — работает всяко.
Что такое find и как оно так произошло?
Команду find следует разобрать отдельно. Это одна из самых часто используемых команд в скриптах RouterOS. Она возвращает внутренний(это не тот номер, что возвращает print, но указывает на тот же объект) номер/номера объектов удовлетворяющих условию.
Если ввести команду(как в первой строке):
/ip dhcp-server lease find address=192.168.254.21
в надежде увидеть номер, то ничего не произойдет. Это как вызвать функцию, а возвращаемый ей результат не обработать.
Поэтому, используем :put для вывода возвращаемого значения(вторая строка). На выходе получаем шестнадцатеричное число DBB. Его можно использовать в качестве значения numbers(раньше мы брали номер из команды print).
Если вы внимательно посмотрите, то заметите что во второй строке
:put [ip dhcp-server lease find address=192.168.254.21]
перед ip нет символа /Эта команда выполнится, так как мы и так находимся в корне иерархии объектов. Но если нам перейти в другой контекст, то тут-же получим ошибку.
Не много отвлеклись, но вернемся к нашей команде
/ip dhcp-server lease set [find address=192.168.254.21] disabled=yes
Что здесь происходит:
-
Устанавливается контекст /ip dhcp-server lease
-
Внутри контекста(пункт 1) выполняется find address=192.168.254.21 и возвращается значение *dbb, как если бы выполнили команду
/ip dhcp-server lease find address=192.168.254.21
-
Внутри контекста(пункт 1) выполняется set(со значением numbers равным *dbb), который присваевает параметру disabled значение yes
В развернутом виде это аналогично:
/ip dhcp-server lease set [/ip dhcp-server lease find address=192.168.254.21] disabled=yes
Таким образом мы видим, что квадратные скобки позволяют выполнять команды во время выполнения других команд и передавать результат «на верх».
Скрипт
На основе вышесказанного напишем скрипт получающий IP адреса клиентов DHCP сервера, отвечающих следующим условиям:
-
Зарезервированы, т.е. IP-адрес закреплен за MAC
-
В настоящее время в сети и имеют активную аренду адреса.
Полученные адреса поместить в список доступа(aclGrantIPs). Если клиент отключился, то удалить его IP из этого списка.
Скопируем скрипт и вставим его в терминал:
/ip dhcp-server lease
:foreach i in=[find] do={
:local addrTMP [get $i address]
:if ([get $i status]="bound" && ![get $i dynamic]) do={
:do {/ip firewall address-list add address=$addrTMP list=aclGrantIPs} on-error={}
} else={
:do {/ip firewall address-list remove [find address=$addrTMP list=aclGrantIPs]} on-error={}
}
}
Разберем построчно, что здесь происходит.
Устанавливаем контекст /ip dhcp-server lease
В цикле перебираем все выделенные адреса.
Если в терминале выполнить :put [/ip dhcp-server lease find]
то будет выведен массив внутренних идентификаторов объектов/записей аренды адресов.
Это и проделывается в строке
:foreach i in=[find] do={
что эквивалентно
:foreach i in=[/ip dhcp-server lease find] do={
или в рассматриваемом случае(со значениями)
:foreach i in=[*d;*26;*86;*141;*143;*1db;*1f3;*dbb;*dc1] do={
Создаем и инициализуем локальную переменную addrTMP значением содержащим IP адрес клиента. Обратите внимание, при создании переменной знак $ в начале не ставится, а при использовании ставится.
:local addrTMP [get $i address]
что эквивалентно
:local addrTMP [/ip dhcp-server lease get $i address]
где $i по порядку подставляется из массива [*d;*26;*86;*141;*143;*1db;*1f3;*dbb;*dc1] циклом :foreach
Аналогично предыдущему шагу, проверим значение параметров status и dynamic.
:if ([get $i status]="bound" && ![get $i dynamic]) do={
Наблюдая за поведением DHCP сервера, на вкладке Leases, можно предположить, что:
- параметр status со значением bound говорит нам о том, что клиент недавно запросил/подтвердил свой адрес.
- параметр dynamic определяет, что адрес динамический и возвращает булево значение true или false соответственно.
Если выполняются условия, что это DHCP клиент, со статической привязкой адресов и активен в настоящее время, то добавляем его в список доступа командой:
/ip firewall address-list add address=$addrTMP list=aclGrantIPs
Причем заметьте, что команда обернута в обработчик ошибок:
:do {} on-error={}
Это сделано, что-бы скрипт не останавливался «в тихую» с ошибкой, в случае если адрес уже есть в списке.
Если условие не выполняется, то удаляем запись с этим IP из списка. Например, наш клиент отключился, или у него администратор снял статическое резервирование адреса.
/ip firewall address-list remove [find address=$addrTMP list=aclGrantIPs]
Для удаления записи используется команда remove. Которая как get, set и еще некоторые другие команды, использует в качестве параметра numbers внутренний номер объекта. В этом случае, объект - это запись в IP - Firewall - Address List.
Команда find может искать по нескольким параметрам в одном запросе.
PS Теперь, когда многие вещи прояснились, можно ознакомиться с официальным описанием скриптового языка RouterOS или его переводом.
PSS Помнится я писал - "О_о, а откуда взялся get? Об этом позже...". Я сам не знаю почему так :) Возможно когда-нибудь узнаю и здесь напишу.
Работа с массивами в скриптах Mikrotik
Работа с массивами в скриптах MikrotikПока память свежа, напишу как работать с массивами в RouterOS. Все команды выполняются в терминале обычным копипастом.
Основы
В самом простом случае массив объявляется так:
:global array1 [:toarray "1,2,3"]
:global arrayNum {1;2;3}
:global arrayStr {"n1";"n2";"n3"}
Строковые значения без пробелов почти везде можно не закрывать кавычками. Но для повышения "читабельности" кода и этого маленького "почти", лучше все-таки использовать кавычки.
Количество элементов в массиве вычисляет функция :len:
# эта команда ничего не выведет в терминал
:len $arrayStr
# а вот эта выведет число 3, указывающее на количество элементов в массиве
:put [:len $arrayStr]
Далее будет использоваться команда :put для вывода значения в терминал. Но в скриптах эта команда обычно не используется, разве только для отладки.
Нумерация элементов массива начинается с нуля. Получить элемент из массива по его индексу можно командой :pick:
:put [:pick $arrayStr 1]
# или
:put ($arrayStr->1)
# в обоих случаях результатом будет: n2
Причем единицу можно заменить переменной, например $i. На всякий случай поясню, здесь и далее, текст "[admin@xxx] >" показывает приглашение терминала и его вводить не требуется. Следующие строки показывают результат выполнения команды.
[admin@xxx] > :for i from=0 to=([:len $arrayStr]-1) do={:put ($arrayStr->$i)}
n1
n2
n3
Ну и конечно есть оператор foreach: который перебирает все элементы массива.
[admin@xxx] > :foreach i in=$arrayStr do={:put $i}
n1
n2
n3
Установить значение элемента в массиве, даже если элемента с таким индексом не существует, можно следующим образом:
[admin@xxx] > :set ($arrayStr->4) "n5"
Теперь посмотрим что получилось в массиве. Выведем значения и тип хранимого значения:
[admin@xxx] > :foreach i in=$arrayStr do={:put "$i - type of item:$([:typeof $i])"}
n1 - type of item:str
n2 - type of item:str
n3 - type of item:str
- type of item:nothing
n5 - type of item:str
Продвинутые методы
В моем скрипте быстрой настройки IPSEC есть такой код(лишь с той разницей, что там используется локальная переменная) объявляющий структуру предприятия:
:global structNET {
"Company Z: OfficeHQ"={ip=23.5.230.7 ; lan=192.168.11.0/24 ; rootnode=true} ;
"Company Z: Office2"={ip=189.2.134.7 ; lan=192.168.12.0/24} ;
"Company Z: Office3"={ip=95.36.71.7 ; lan=192.168.13.0/24} ;
"Company Z: Office4"={ip=154.4.96.7 ; lan=192.168.14.0/24 ; initiator=true ; rootnode=true} ;
}
Обращаться к элементам такого массива можно следующим образом:
[admin@xxx] > :put ($structNET->"Company Z: Office4")
initiator=true;ip=154.4.96.7;lan=192.168.14.0/24;rootnode=true
[admin@xxx] > :put ($structNET->"Company Z: Office4"->"ip")
154.4.96.7
Естественно, что вместо литералов могут использоваться переменные, как было показано в примере выше, где 1 заменялась на $i. А вот конструкция с :pick , может преподнести сюрприз:
[admin@xxx] > :put [:pick $structNET 0]
ip=189.2.134.7;lan=192.168.12.0/24
Подозреваю, это связано с сортировкой именованных элементов массива. И вывод foreach следующего примера это доказывает.
Перебор всех элементов при помощи :foreach:
[admin@xxx] > :foreach office,data in=$structNET do={ :put $office; :put $data;}
Company Z: Office2
ip=189.2.134.7;lan=192.168.12.0/24
Company Z: Office3
ip=95.36.71.7;lan=192.168.13.0/24
Company Z: Office4
initiator=true;ip=154.4.96.7;lan=192.168.14.0/24;rootnode=true
Company Z: OfficeHQ
ip=23.5.230.7;lan=192.168.11.0/24;rootnode=true