Программирование в RouterOS

Программирование в RouterOS divan1 пт, 25 янв 2019 - 19:05

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

Mikrotik, вторые шаги

Mikrotik, вторые шаги admin вс, 09 апр 2017 - 23:51

Предисловие. Это можно не читать ))

Есть много хороших и не очень руководств по языку скриптов Mikrotik, но подробного описания как работать с функционалом(объектами) я не нашел. Пришлось собирать информацию по крупицам, разбираться по примерам. Как известно, что бы закрепить и систематизировать полученные знания, полезно их рассказать другим. Поэтому, рискну изложить то, что понял сам из разрозненных источников.

На первый взгляд может показаться, что очень много написанного не имеет отношения к написанию скриптов. Но это только на первый взгляд ;)

Подготовка

Хоть я и старался максимально подробно изложить материал, но перед началом работы имеет смысл ознакомиться с основами работы в терминале Mikrotik. Это и будут первые шаги. Вот достойный материал на эту тему: http://mikrotik.vetriks.ru/wiki/%D0%94%D0%BB%D1%8F_%D0%BD%D0%B0%D1%87%D…

Поехали

Для примера будем работать с DHCP сервером.

Окно выбора настроек DHCP сервера

Открываем терминал, пункт меню «New Terminal» и вводим команду: /ip dhcp-server  и жмем кнопку табуляции(далее <TAB>). Получим такой вывод:

/ip dhcp-server

Здесь мы видим доступные для продолжения ввода команды значения. То что подсвечено «синим» цветом, это подразделы. В WinBox их можно увидеть в виде вкладок. Нас же, более всего интересуют «фиолетовые» команды. Пробуем набирать print и <ENTER>. На выводе получим список всех(в примере один) настроенных DHCP серверов:

/ip dhcp-server print

Кстати, после print то же можно нажать <TAB>. Поиграйтесь с разными параметрами.

Усложняем задачу. Идем на вкладку Leases. В этом примере она выглядит так:

Окно настроек резервирования DHCP сервера

Отключим(не удалим(remove), а именно отключим(disable)) резервирование IP адреса для хоста, у которого текущий адрес 192.168.254.21. На скриншоте мы видим результат - команды, их вывод и отключенная запись резервирования. Разберем подробно.

Окно настроек резервирования DHCP сервера + терминал

Вводим: /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 ее-то мы и будем использовать.

Обратите внимание на эти команды:

/ip dhcp-server lease

В первой строке: От корня иерархии объектов микротика мы двигаемся в раздел 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

Окно настроек резервирования DHCP сервера + терминал

Если кто заметил, в тексте IP-адрес экранирован кавычками, а на скриншоте нет. Можно использовать кавычки, можно не использовать — работает всяко.

Что такое find и как оно так произошло?

Команду find следует разобрать отдельно. Это одна из самых часто используемых команд в скриптах RouterOS. Она возвращает внутренний(это не тот номер, что возвращает print, но указывает на тот же объект) номер/номера объектов удовлетворяющих условию.

Окно настроек резервирования DHCP сервера + терминал

Если ввести команду(как в первой строке):
/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

 

Не много отвлеклись, но вернемся к нашей команде
/ip dhcp-server lease set [find address=192.168.254.21] disabled=yes

Что здесь происходит:

  1. Устанавливается контекст /ip dhcp-server lease

  2. Внутри контекста(пункт 1) выполняется find address=192.168.254.21 и возвращается значение *dbb, как если бы выполнили команду /ip dhcp-server lease find address=192.168.254.21

  3. Внутри контекста(пункт 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 сервера, отвечающих следующим условиям:

  1. Зарезервированы, т.е. IP-адрес закреплен за MAC

  2. В настоящее время в сети и имеют активную аренду адреса.

Полученные адреса поместить в список доступа(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={}
    }
}

Окно настроек резервирования DHCP сервера + терминал

Разберем построчно, что здесь происходит.

Устанавливаем контекст /ip dhcp-server lease

В цикле перебираем все выделенные адреса.

Если в терминале выполнить :put [/ip dhcp-server lease find] то будет выведен массив внутренних идентификаторов объектов/записей аренды адресов.

: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 divan1 пт, 25 янв 2019 - 03:01

Пока память свежа, напишу как работать с массивами в 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