Я использую Mikrotik в качестве домашнего и офисного маршрутизатора, и в целом система очень нравится. RouterOS имеет широкие возможности, которые покрывают 90% моих задач, если чего-то недостает, то можно «дописать» функционал с помощью внутренних скриптов. Но когда начинаешь писать более-менее вменяемый скрипт или пытаешься понять и применить чужой рецепт, становятся заметны очертания подводной части айсберга, всплывают странные особенности языка.
Я провел небольшое исследование переменных в скриптах Mikrotik, рассмотрел под лупой объявление и инициализацию.
Получилась, на мой взгляд, достойная тема для написания статьи. Итак, приступим.
Что же нам говорит Manual:Scripting о переменных в скриптах? А говорит он нам, что переменные бывают двух областей видимости: локальные и глобальные, что объявляются они командами —
:local <name> <value>
и
:global <name> <value>
Предлагаю сразу сосредоточиться на global переменных, так как их легче исследовать за счет их лучшей «наблюдаемости», а большинство выводов, думаю, можно спокойно перенести и на local.
Прежде чем мы объявим первую глобальную переменную, давайте ознакомимся с особенностями некоторых встроенных типов данных и символьных конструкций, которые можно использовать как значения переменных или аргументы операторов сравнения.
Manual:Scripting говорит нам, что есть около десятка типов, из которых мы рассмотрим только некоторые, важные для понимания переменных. Итак, явные и логичные типы: числовой num, строковый str и массив array. Далее упоминается тип (оно же — значение переменной) nil, про который написано, что он будет у переменной по умолчанию, если ей ничего не присвоено. Поверим.
Если поработать с командной консолью в WinBox, можно заметить, что есть еще одно странное ключевое слово nothing, которое непонятно что означает, какое-то «ничего».
[admin@MikroTik] > :global var0
[admin@MikroTik] > :put [:typeof $var0]
nothing
[admin@MikroTik] > /environment print
var0=[:nothing]
Ну и где тут nil? Зато полно nothing.
Исследование nil
В общем, чтобы разобраться с этими особыми типами и значениями, применим системный подход, проанализируем выдачу однотипных запросов, но с разными наборами исходных данных. А исходными данными у нас будут в начале разные «странные» константы и выражения. В результате получается такая таблица:
№ | Что это? | <value> | <value2> | :if (<value> = <value2>) do={:put TRUE} else={:put FALSE} | :put [:typeof <value>] | :put [:typeof [<value>]] | :put [:len <value>] | :put [:len [<value>]] |
---|---|---|---|---|---|---|---|---|
1 | Nothing в командной обертке | [] / [:nothing] / или даже так [:] | [] | TRUE | nil | nil | 0 | 0 |
2 | Пустая строка | "" | "" / {} | TRUE | str | expected command name | 0 | 0 |
3 | Nothing внутри выражения | (:nothing) / или даже так (:) | [] / "" / {} | FALSE | nothing | nil | 0 | 0 |
4 | Массив из nothing | {:nothing} / {:} | {} / "" | TRUE | array | nil | 1 | 1 |
5 | Элемент массива nothing | ({:nothing}->0) | [] / "" / {} | FALSE | nothing | nil | 0 | 0 |
Тут даны результаты сравнения различных констант выражений и команд над константами между собой. Понимание свойств констант поможет при написании правых частей операторов сравнения.
На счет полей таблицы. В языке скриптов Mikrotik квадратные скобки обозначают встраивание результата команды в общее выражение, поэтому в общем смысле value != [value], это важно.
Прокомментирую построчно:
Строка 1: все варианты записи поля value синонимичны! В будущем я предлагаю использовать лаконичное []. Повторюсь, что квадратные скобки обозначают встраивание результата команды в общее выражение. Как видите, это один из способов «генерации» nil не в чистом виде, а как результат пустой команды в квадратных скобках, который как раз равен nil.
Строка 2:
Тут все просто. Пустая строка вполне очевидная вещь. Единственные момент, что она оказывается равна массиву из ничего, но предлагаю не обращать на это особого внимание. И нельзя использовать пустую строку как команду в квадратных скобках, это единственное место, где терминал не проглотил синтаксис по исходным данным таблицы, логично.
Строка 3:
Круглые скобки несут в себе выражение, внутри можно тоже поместить nothing, и результат от такого выражения тоже будет nothing, по сути это почти чистый nothing. Видно, что тип результата выражения (:nothing) дает nothing, а тип от обертки из командных скобок [] дает nil, по аналогии со строкой 1. Вообще после повторного осмысления написанного, я понял что это спекуляция, т.к. внутрь () можно поместить что угодно, любой бессмысленный набор символов, главное, что результат (...) дает nothing.
Строка 4:
Массив, который содержит ничего, на самом деле содержит 1 элемент. Тип, как и ожидалось, array, тип результата командной обертки также дает nil
Строка 5:
Добираемся до элемента массива из ничего, прослеживается полная аналогия выдачи со строкой 3. В общем такой элемент — это тоже чистое nothing.
Какие общие выводы можно сделать из этого странного brainfuck-а. nothing действительно существует, им можно оперировать, редкие выражения могут его возвращать, но команды в [] никогда не возвращают nothing, а только nil. Другой более важный вывод, что оператор длины значения или кол-ва элементов массива :len ведет себя очень стабильно и генерирует предсказуемый результат, поэтому его я могу однозначно рекомендовать для использования в скриптах, когда требуется проверка возвращаемых выражениями и командами значений. И что [] = [:nothing] = nil.
Таблица дает представление о том, что могут возвращать различные команды и выражения языка Mikrotik.
Исследование объявлений переменных
Теперь перейдем к более практическому объявлению переменных.
№ | Что это? | Объявление | <val> | <val2> | :put <val> | :if (<val> = <val2>) do={:put TRUE} else={:put FALSE} | :put [:typeof <val>] | :put [:typeof [<val>]] | :put [:len <val>] оно же :put [:len [<val>]] | /environment print |
---|---|---|---|---|---|---|---|---|---|---|
1 | Без присвоения | :global var1 | $var1 | [] / (:) | FALSE / TRUE | nothing | nil | 0 | var1=[:nothing] | |
2 | Присвоение, удаляющее переменную | :global var2 (:nothing) | $var2 | (:) | TRUE | nothing | nil | 0 | - | |
3 | Присвоение nil | :global var3 [] | $var3 | [] | TRUE | nil | nil | 0 | var3=[] | |
4 | Присвоение пустой строки | :global var4 "" | $var4 | "" / {} | TRUE / TRUE | str | str | 0 | var4="" | |
5 (3) | Странный nil, аналог 3 | :global var5 [{}] | $var5 | [{}] / [] | TRUE / TRUE | nil | nil | 0 | var5=[] | |
6 | Массив из nothing | :global var6 {:} | $var6 | "" / {""} / {} | TRUE / TRUE / TRUE | array | array | 1 | var6={[:nothing]} | |
7 (6, 8) | Массив из пустой строки | :global var7 {""} | $var7 | "" / {""} / {} | TRUE / TRUE / TRUE | array | array | 1 | var7={""} | |
8 (6, 7) | Массив из nil | :global var8 {[]} | $var8 | "" / {""} / {} | TRUE / TRUE / TRUE | array | array | 1 | var8={[]} | |
№ | Что это? | Объявление | <val> | <val2> | :put <val> | :if (<val> = <val2>) do={:put TRUE} else={:put FALSE} | :put [:typeof <val>] | :put [:typeof [<val>]] | :put [:len <val>] оно же :put [:len [<val>]] | /environment print |
9 | Присвоение числа | :global var9 123 | $var9 | н./и. | 123 | н./п. | num | num | 3 | var9=123 |
10 | Присвоение строки | :global var10 "987" | $var10 | н./и. | 987 | н./п. | str | str | 3 | var10="987" |
11 | Массив из одного числа | :global var11 {555} / :global var11 {555;} | $var11 | н./и. | 555 | н./п. | array | array | 1 | var11={555} |
12 | Массив разнородных элементов | :global var12 {33;"test123"} | $var12 | н./и. | 33;test123 | н./п. | array | array | 2 | var12={33; "test123"} |
13 | Элемент массива | -//- | ($var12->0) | н./и. | 33 | н./п. | num | num | 2 | -//- |
14 | Элемент массива | -//- | ($var12->1) | н./и. | test123 | н./п. | str | str | 7 | -//- |
15 | Массив c nothing элементом | :global var13 {33;(:)} | $var13 | н./и. | 33; | н./п. | array | array | 2 | var13={33; [:nothing]} |
16 | Элемент массива | -//- | ($var13->0) | н./и. | 33 | н./п. | num | num | 2 | -//- |
17 | Элемент массива | -//- | ($var13->1) | н./и. | н./п. | nothing | nil | 0 | -//- | |
18 | Массив c nil элементом | :global var14 {1012;[]} | $var14 | н./и. | 1012; | н./п. | array | array | 2 | var14={1012; []} |
19 | Элемент массива | -//- | ($var14->1) | н./и. | н./п. | nil | nil | 0 | -//- |
н./и. — не используется; н./п. — не применимо
Построчные комментарии к таблице:
1. Переменная создана, но ей ничего не присвоено. Переменная как бы содержит nothing.
2. Если переменной присвоить такое выражение, то это приведет к удалению глобальной переменной из переменных окружения.
3. Стандартный способ создания пустых переменных. Переменная содержит nil.
4. Присвоение пустой строки. Тут все очевидно.
5. Получается, что такое выражение аналогично [] простому присвоению nil, как в 3. Думаю, это потому, что внутри [] несуществующая команда и результат этой команды дает nil
6, 7, 8. Присвоение фигурных скобок делает из переменной массив, хоть и пустой. Обратите внимание, что записи имеют одинаковые результаты в таблице, но это не касается свойств элементов этих массивов. Свойства элементов массивов рассмотрены ниже.
9, 10. Простые типы данных. Все довольно очевидно.
11. Массив из одного элемента, обратите внимание, что {555;} по результатам равно {555}
12, 13, 14. В массив могут входить элементы разных типов данных. Исследование свойств элементов массива дает предсказуемые результаты.
15, 16, 17. Один из элементов массива nothing. Элементы массива обладают теми же свойствами, что и просто переменные и константы данных типов и значений. Прослеживается аналогия с пунктом 2.
18, 19. Прослеживается аналогия с пунктом 3.
Исследование, на мой взгляд, получилось немного спорным, но я очень надеюсь, оно внесет больше порядка в ваше понимание Mikrotik, чем хаоса. В качестве дополнительной компенсации публикую скрипт для работы с динамическим DNS замечательного сервиса FreeDNS.afraid.org.
Скрипт для FreeDNS.afraid.org
Я видел несколько аналогичных скриптов, но они не понравились мне из-за разных ограничений, поэтому я решил собрать свой велосипед, который меня бы полностью устроил.
За основу я взял скрипт от LESHIYODESSA. Мне не очень понравился его алгоритм, в котором использовался файл для хранения текущих адресов записей Dynamic DNS и производился его периодический парсинг, кроме того, скрипт не поддерживает обновление разных записей, для этого предлагается размножить скрипт, но это не снимает проблему обновления записи по заданному IP-адресу. Поэтому фактически я написал свой собственный скрипт, в котором заменил работу с файлами на более надежный механизм периодического обновления (с часовым интервалом) и форсированное обновление по изменению отслеживаемых IP-адресов интерфейсов, полученных по DHCP, независимое для нескольких записей.
Объявляем массивы имен субдоменов FreeDNS.afraid.org и их хешей, имен WAN-инетфейсов, у которых мы будем отслеживать IP-адреса. А также задаем кол-во записей (Quant) по размеру массива либо вручную:
:local SubdomainHashes {"U3dWVE5V01TWxPcjluEo0bEtJQWjg5DUz=";"U3pWV5VFTWxPcjlOEo0EtJpOE1MAyDc="}
:global DNSDomains {"aaa.xyz.pu";"bbb.xyz.pu"}
:global WANInterfaces {"ether4-WAN-Inet";"ether3-WAN-Beeline"}
:global Quant [:len $DNSDomains]
Объявляем вспомогательные переменные:
SkipCounters — массив счетчиков проверок отслеживаемых интерфейсов
LastIPs — массив IP-адресов, которые уже были отправлены в FreeDNS.
Массив счетчиков позволит обновлять Dynamic DNS записи независимо друг от друга.
Сначала я делаю пустое объявление переменной :global SkipCounters, такое объявление позволяет либо создать новую глобальную переменную, либо использовать уже существующую в переменных окружения и ее значение без перезаписи.
Следующие конструкции работают для только что созданных переменных, проверяется тип данных, если он не массив, то тип меняется на массив, и присваиваются значения переменных. Таким образом на выходе мы имеем проинициализированные нужными значениями переменные типа массив.
:global SkipCounters
:if ([:typeof $SkipCounters] != "array") do={
:set SkipCounters {""}
:for i from=0 to=($Quant-1) do={:set ($SkipCounters->$i) 1}
}
:global LastIPs
:if ([:typeof $LastIPs] != "array") do={
:set LastIPs {""}
:for i from=0 to=($Quant-1) do={:set ($LastIPs->$i) ""}
}
Ни и собственно сам алгоритм отслеживания-обновления.
Получаем текущий IP-адрес из dhcp-client. Дальше самое интересное.
Команда [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address] в общем случае может вернуть что угодно кроме строки, содержащей IP-адрес, поэтому она в норме обновит значение переменной CurrentIP. Возвращаемым значением может быть либо строка с IP, либо nil, или будет ошибка выполнения и команда не обновит CurrentIP. Поэтому я строкой выше ввожу явное объявление :local CurrentIP "". И после выполнения команды в CurrentIP будет либо "", либо nil, либо IP-адрес.
Как я писал выше, наибольшей устойчивостью обладает оператор :len, поэтому используем его дальше для проверки адекватности полученных данных [:len $CurrentIP] > 0. Еще отслеживаем значение счетчика, и если он >=60, принудительно отсылаем запрос в FreeDNS. Таким образом повышается устойчивость алгоритма к проблемам связи. Скрипт в шедулере у меня выполняется раз в минуту, поэтому период обязательного обновления около 1 часа, что не сильно обременяет сервис FreeDNS.
На что еще стоит обратить внимание. В URL запроса на обновление присутствует параметр "&address=".$CurrentIP, этот параметр позволяет явно указать IP-адрес для субдомена вместо автоматического (по интерфейсу с которого ушел запрос).
:for i from=0 to=($Quant-1) do={
:local CurrentIP ""
:set CurrentIP [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address]
:set CurrentIP [:pick $CurrentIP 0 ([:len $CurrentIP]-3)]
# :log info ("Current SkipCounter$i: ".($SkipCounters->$i))
:if ([:len $CurrentIP] > 0 and ($CurrentIP != ($LastIPs->$i) or ($SkipCounters->$i) > 59)) do={
:if ($CurrentIP != ($LastIPs->$i)) do={
:log info ("Service Dynamic DNS: Renew IP: ".($LastIPs->$i)." for ".($DNSDomains->$i)." to $CurrentIP")
}
/tool fetch url=("http://freedns.afraid.org/dynamic/update.php?".($SubdomainHashes->$i)."&address=".$CurrentIP) keep-result=no
:set ($LastIPs->$i) $CurrentIP
:set ($SkipCounters->$i) 1
} else={
:set ($SkipCounters->$i) (($SkipCounters->$i) + 1)
}
}
# MultiFreeDNS
:local SubdomainHashes {"U3dWVE5V01TWxPcjluEo0bEtJQWjg5DUz=";"U3pWV5VFTWxPcjlOEo0EtJpOE1MAyDc="}
:global DNSDomains {"aaa.xyz.pu";"bbb.xyz.pu"}
:global WANInterfaces {"ether4-WAN-Inet";"ether3-WAN-Beeline"}
:global Quant [:len $DNSDomains]
:global SkipCounters
:if ([:typeof $SkipCounters] != "array") do={
:set SkipCounters {""}
:for i from=0 to=($Quant-1) do={:set ($SkipCounters->$i) 1}
}
:global LastIPs
:if ([:typeof $LastIPs] != "array") do={
:set LastIPs {""}
:for i from=0 to=($Quant-1) do={:set ($LastIPs->$i) ""}
}
:for i from=0 to=($Quant-1) do={
:local CurrentIP ""
:set CurrentIP [/ip dhcp-client get [find where interface=($WANInterfaces->$i)] address]
:set CurrentIP [:pick $CurrentIP 0 ([:len $CurrentIP]-3)]
# :log info ("Current SkipCounter$i: ".($SkipCounters->$i))
:if ([:len $CurrentIP] > 0 and ($CurrentIP != ($LastIPs->$i) or ($SkipCounters->$i) > 59)) do={
:if ($CurrentIP != ($LastIPs->$i)) do={
:log info ("Service Dynamic DNS: Renew IP: ".($LastIPs->$i)." for ".($DNSDomains->$i)." to $CurrentIP")
}
/tool fetch url=("http://freedns.afraid.org/dynamic/update.php?".($SubdomainHashes->$i)."&address=".$CurrentIP) keep-result=no
:set ($LastIPs->$i) $CurrentIP
:set ($SkipCounters->$i) 1
} else={
:set ($SkipCounters->$i) (($SkipCounters->$i) + 1)
}
}
Автор: Chupakabra303