New possibilities in scripts Router OS 7 Mikrotik / Habr

New possibilities in scripts Router OS 7 Mikrotik / Habr

The appearance of Router OS 7 has been long awaited. The new system was created with obviously more productive devices, with new processors and more RAM. A new Linux kernel, support for WireGuard and ZieroTier, new routing and BGP capabilities, a new users manager, MPLS updates, running applications in containers is not a complete list of new features.

But in this article we will discuss the innovations only in the scripting language of the system up to the current beta version 7.12.

So, version 7 (as of 7.12 beta 3) got some new commands:

:timestamp

The need for a team is quite obvious. All work with time must be done by computing Universal UNIX Time equal to the number of seconds since midnight (00:00:00 UTC) on January 1, 1970; this moment is called the “Unix era”. Previously, including in all branches of Router OS 6, it was necessary to programmatically calculate UNIX time using a bulky script. Now it only takes one command:

:put [:timestamp]

,

which calculates Unix time taking into account the user’s time zone (GMT offset) except leap year seconds.
However, I do not like the format of the conclusion. I don’t know why, Ros 7 also changed the time format. Why do RouterOS developers support calculating the number of weeks?

The command above outputs:

2801w06:05:12.725909345

Well, and how to understand it?

For readability, you can use another novelty – the command :tonsec. Note that it is: tonsec, not: tosec. The command converts the argument to a number of nanoseconds. Thus, the following transformation can be used for calculations:

:put [:tonsec [:timestamp]]

We will get the following result:

1694066733047820422

Essentially excluding GMT

[:tonsec [:timestamp] = [:tonsec ([:totime [/system clock get date]]+[/system clock get time])]

The next innovation is the team :rndnum serves to generate a random number. It cannot be used without parameters and requires specifying a range from which to select a random number.

:rndnum from=[num] to=[num]

Example:

:put [:rndnum from=1 to=99]

Conclusion:

17

Good for generating keys, passwords, sampling random values ​​from a range, whatever.

:rndstr from=[str] length=[num]

The command to generate a random string. Can be used without the length parameter, then the string length defaults to 16 characters.

:put [:rndstr from="abcdef%^&" length=33]

e^&b^accaefcbf&b%^%e&ac%feda%&%ac

Symbols for generation are taken only from specified in “from=”. The command is also very useful for generating keys and passwords.

For example, generating a random MAC address can now be easier than in Router OS 6 (examples taken from this official Mikrotik forum post:

{
:local array { "c8:ea:f8"; "70:9f:2d"; "38:3b:26"; "5c:fa:fb"; "9c:7b:ef" }   
:local part1 ($array->[:rndnum from=0 to=([:len $array]-1)])

:local hex1 [:rndstr length=6 from="0123456789abcdef"]
:local part2 ([:pick $hex1 0 2].":".[:pick $hex1 2 4].":".[:pick $hex1 4 6])

:local Mac ($part1.":".$part2)
:put $Mac
}

Or in general a script in one line:

:put "$[:rndstr length=1 from="0123456789ABCDEF"]$[:rndstr length=1 from="048C"]$[:rndstr length=10 from="0123456789ABCDEF"]"

:retry command = delay =[num] max=[num] on-error=

The new :retry command is designed to execute the instructions in the “command” block with the maximum number of retries specified in the max= parameter with a specified delay between retries delay=. For errors in the execution of the command block, the on-error={} block is executed

Example:

:retry delay=1s max=3 on-error={:put "got an error"} command={:put [/system/identity/get name]}

# Mikrotik

Or:

:retry delay=1s max=3 on-error={:put "I give up!"} command={:put "trying..."; :error "cause error"}   

# trying…
# trying…
# trying…
# I give up!

:jobname

Finally, there is a command that allows you to find out the name of the script without “dancing with tambourines”.
Previously, in Router OS version 6, you could use the following trick for this (by Rextended, https://forum.mikrotik.com/viewtopic.php?t=197314#p1009493):

:local UniqueScriptID "QnJhdm8h"
:local ThisScriptName [/system script get ([find where source~"$UniqueScriptID"]->0) name]
:local AlreadyRunning "Script $ThisScriptName already running"
:if ([:len [/system script job find where script=$ThisScriptName]] > 1) do={:log error $AlreadyRunning; :error $AlreadyRunning}

The script searched for its name by a unique tag in its own body.
Now, in Router OS 7, you can:

/system script add name=log-jobname source=\"/log info \"\$[:jobname]\""

Or just from script code

 :log info [:jobname]

Approached the most difficult new team :convert from=[arg] to=[arg] transform=
The command converts the specified value from one format to another. By default, the automatically parsed value is used if the “from” format is not specified (eg “001” becomes “1”, “10.1” becomes “10.0.0.1”, etc.). The transform option is not used by default, which is equivalent to transform = none. When transforme=reverse is specified, the transformed data is reversed.
from specifies the value format – base32, base64, hex, raw, rot13, uri.
to specifies the format of the output value – base32, base64, hex, raw, rot13, uri.

Examples:

:put [:convert 001 to=hex ]

31

:put [:convert [/ip dhcp-client/option/get hostname raw-value] from=hex to=raw ]

MikroTik

:put [:convert "abcd" to=base64]   

# YWJjZA==

:put [:convert from=base64 "YWJjZA==" ]   

# abcd

:put [:convert from=base64 "YWJjZA==" transform=reverse]

# dcba

:put [:convert from=base64 "YWJjZA==" transform=reverse to=hex] 

#64636261

Some of the new commands touched only the console.
A new instruction has appeared to simplify entering values

:terminal/ask preinput= prompt= which allows you to enter the value after it is issued to the preinput terminal with the request of the prompt parameter.

Example:

:put [:terminal/ask "Do you agree to use Router OS 7?"]

In this case, the preinput is given without the codeword, and the prompt is omitted.
To enter a line from the Terminal, including in both branches of the system (6 and 7), of course, you can write a small script using :terminal inkey of the following type:

:local EnterString do={
     :local cont; :local string
        :while ($cont!=13) do={
          :if ([:len $string]<254) do={
             :local key ([:terminal inkey])
                :if ($key!=13) do={
                     :local char [[:parse "(\"\\$[:pick "0123456789ABCDEF" (($key >> 4) & 0xF)]$[:pick "0123456789ABCDEF" ($key & 0xF)]\")"]]
             :set string ("$string"."$char")}
             :set cont $key
         }
      }
:return $string}

This small local function allows you to enter a string of up to 254 characters character by character. In this example, input is stopped if the user presses the “Enter” key (Enter key code=$0D (13). Clearly character-by-character input allows you to use any other constraints on the end of input (such as another terminating character or the number of characters to be typed), which allows for the most flexible setting of the input.
It can be supplemented with various buns with an instant check of the entered characters, limit the length of the formed line, etc.

But, if you need it easier and faster, you can use the following feature:

:local input do={:put $1; :return;}
:local login [$input "Enter login:"]
:local password [$input "Enter password:"]
:put "Login is [$login] and password is [$password]"

It works like this:

Enter login
value: admin
Enter password
value: test
Login is [admin] and password is [test]

The trick is that an empty :return from the Terminal always prompts you to enter a value. The end of the input must be the “Enter” key. The only inconvenience will be the output of the word “value:” as a prompt before typing, but if it is not annoying, then everything works fine.
It is convenient to use interactive scripts to adjust configurations, etc.

In Router OS 7, starting with version 7.11, another possibility appeared – use :terminal ask. This new command eliminates the inconvenience of prompting for value: value. The end of the input must also be the “Enter” key.

:global Answer [:terminal/ask "Do you agree to use Router OS 7?"]
:put $Answer

Or something like this (for example):

{
:global Answer []
:while ($Answer!="yes") do={
:set Answer [:terminal/ask "Do you agree to use Router OS 7?"]
}
}

You can also specify any prompt for input if you use the “preinput” option:

:global userinput [/terminal/ask preinput="preinput>" prompt="Some text that in prompt"]

A very useful new command:

:console/inspect as-value request= path=
The instruction allows you to request a list of commands and parameters of the child menu.
The guru of the official Mikrotik forum Amm0 wrote a script that traverses the entire Router OS command tree and receives a list of all commands, parameters and their types:

:global ast [:toarray ""]

:global mkast do={
    :global mkast
    :global ast
    :local path "" 
    :if ([:typeof $1] ~ "str|array") do={ :set path $1 }
    :local pchild [/console/inspect as-value request=child path=$path]
    :foreach k,v in=$pchild do={
        :if (($v->"type") = "child") do={
            :local astkey ""
            :local arrpath [:toarray $path]
            :foreach part in=$arrpath do={
                :set astkey "$astkey/$part"
            }
            :set ($ast->$astkey->($v->"name")) $v
            :put "Processing: $astkey $($v->"name") $($v->"node-type")"
            :local newpath "$($path),$($v->"name")"
    		# TODO use [/console/inspect as-value request=syntax path=$path]
            [$mkast $newpath]
        }
    }
    return $ast
}

# & this call start the recursion 
:put [$mkast]
 :put ($ast->"/ip/address")

add=.id=*2;name=add;node-type=cmd;type=child;comment=.id=*3;name=comment;node-type
=cmd;type=child;disable=.id=*4;name=disable;node-type=cmd;type=child;edit=.id=*5;n
ame=edit;node-type=cmd;type=child;enable=.id=*6;name=enable;node-type=cmd;type=chi
ld;export=.id=*7;name=export;node-type=cmd;type=child;find=.id=*8;name=find;node-t
ype=cmd;type=child;get=.id=*9;name=get;node-type=cmd;type=child;print=.id=*a;name=
print;node-type=cmd;type=child;remove=.id=*b;name=remove;node-type=cmd;type=child;
reset=.id=*c;name=reset;node-type=cmd;type=child;set=.id=*d;name=set;node-type=cmd
;type=child

Another new console instruction :task rumored to be similar to the Linux jobs, fg and bg instructions, which allow you to find out which process is running, move the process to the background or, conversely, make it the first active, etc. But so far, I don’t have exact data on how the team works and what parameters it has.

Some changes affected the team :execute. She got a new attribute.as-string“, which makes it synchronous (e.g. waits for the script to finish before returning with a new “as-string”).

Also, version 7 introduced the option “as-value“for”/tool ​​​​/snmp-get“, which can retrieve values ​​from the SNMP OID script without the parsing required in version 6:

{
   :local interfaceOneName [/tool/snmp-get oid=.1.3.6.1.2.1.2.2.1.2.1 address=127.0.0.1 community=public as-value] 
   :put $interfaceOneName
   :put ($interfaceOneName->"value")
}

# oid=1.3.6.1.2.1.2.2.1.2.1;type=octet-string;value=ether1
# ether1

Finally, the console has been improved when creating complex arrays:

*) console – improved multi-argument property parsing into array;
I don’t know what exactly the developers of Mikrotik had in mind, but the gurus of the Mikirotik forum assure that complex arrays in ROS 7 stopped “falling apart on pointers”, now when copying an array, it is exactly copied in memory (because we remember that the new Mikrotik devices now have enough RAM !), but not creating a pointer. That is, arrays are copies in version 7, not “pointers”. You can talk about (“pass by value” in version 7, instead of “pass by reference” in version 6).
The following script clearly illustrates the problem:

# the mother array
:global aGlobalArray ({});    

# function to add a sub-array, defined locally, to the global array
:global popData do={
    :global aGlobalArray;        
    :local subObj ({soname="soname-orig"; sodata="sodata-orig"});
    :set ($aGlobalArray->"sub-obj-1") $subObj;
    :return "OK";
}

:log info "Populate and log";
:local pop [$popData];
:foreach key,value in=$aGlobalArray do={  
    :log info ("[key:".$key."] soname:".($value->"soname")." | sodata:".($value->"sodata"));
    # result [key:sub-obj-1] soname:soname-orig | sodata:sodata-orig
}

:log info "Modify subjobj and log";
:foreach key,value in=$aGlobalArray do={  
    :set ($value->"sodata") "sodata-modified";
    # logging the data here shows it is modified    
    # Adding this line fixes the issue for v7 but is not needed in v6
    # :set ($aGlobalArray->"$key") $value;
}
:foreach key,value in=$aGlobalArray do={  
    :log info ("[key:".$key."] soname:".($value->"soname")." | sodata:".($value->"sodata"));
    # v6 result [key:sub-obj-1] soname:soname-orig | sodata:sodata-modified
    # v7 result [key:sub-obj-1] soname:soname-orig | sodata:sodata-orig
}

In a simpler example kindly shared by Amm0: if we take a simple array with a child array inside. The largest internal value, ca, starts with “2”, and we create a new array using part of the first, then set the subarray’s internal value to 1:

:global myarray {a=1;child={ca=2}}
:global mysubarray ($myarray->"child")
:set ($mysubarray->"ca") 1

In Router OS version 6, mysubarray actually refers to the parent array, so when the value of mysubarray is set, the parent value of myarray is also updated:

:put ( ($myarray->"child"->"ca") = 1)

#true

In version 7 this changed so mysubarray is a copy of the original array so updates do not affect myarray. So “:set” only sets the one in the subarray. So the parent array is left using its original value of 2 instead of the updated 1.

:put ( ($myarray->"child"->"ca") = 2)

#true

In addition, I want to report news that concerns both branches of the system and 6 and 7. Recently, the user fludorkin discovered a new (undocumented) data type Router OS – Op:

:put [:typeof (>[])]

Op

In the future, this type of data was analyzed and it was suggested that “op” possibly means “only pointer”, and the construction serves as a shortcut for addressing complex elements of associative arrays, including if their elements are functions. You can read more about it here and here.

That’s all for now. We are glad that the Mikrotik developers finally got around to improving the scripting language. Apparently, their hands were tied earlier by the insufficient amount of RAM in the old RouterBoards, which was overcome with the release of new devices such as RB5009, HAP AX2 and HAP AX3, L009, etc. carefully so as not to disrupt the system and, of course, document your developments in detail. Needless to say, beta changelogs are rolled over and not included in the official manuals until stable versions are released. Anyone who has read this article can leave their suggestions for improving the Mikrotik scripting language in the comments. We will certainly discuss the interesting ones on the official forum.

Related posts