I’m new to RouterOS scripting.
While probably not the best way…. this works for me to incrementally shutdown devices via SSH during A/C power loss.
My function variables are super long and I haven’t quite found a way to condense them into a list format for readability.
Any help would be appreciated, and thanks in advance.
:local defThreshold 00:05:00
:local defSrcAddr 192.168.99.1
:local defRemoteHost pve-node1
:local defLocalHostIfaceArray [:toarray "ether7,ether6,ether5,ether4,ether3,ether2"]
:local defNetworkDeviceArray [:toarray "hap2,hap1,rb2,crs2,crs1"]
:local defSrvDomain domain.local
:local defMgmtDomain domain.local
:local defEmail email@example.com
:local defHost ""
:local defDomain ""
:local defCmd ""
:local defUpsName [/system/ups get [find] value-name=name]
:local defUpsOnline ([/system/ups/monitor $defUpsName once as-value]->"on-line")
:local defUpsRuntime ([/system/ups/monitor $defUpsName once as-value]->"runtime-left")
:local defUpsLoad ([/system/ups/monitor $defUpsName once as-value]->"load")
:local funcNetworkShutdown do={
:local host ""
:local domain ""
:local cmd ""
:foreach networkDevice in=$networkDeviceArray do={
:set host $networkDevice
:set domain $mgmtDomain
:set cmd [/system/shutdown]
$funcLogShutdown threshold=$threshold host=$host upsName=$upsName
$funcSshExec srcAddr=$srcAddr host=$host domain=$domain cmd=$cmd
:delay 3s
}
:set host [/system identity get name]
$funcLogShutdown threshold=$threshold host=$host upsName=$upsName
$cmd
}
:local funcLocalHostShutdown do={
:local host ""
:local domain ""
:local cmd ""
:if ($localHostArray != "") do={
:foreach localHost in=$localHostArray do={
:set host $localHost
:set domain $srvDomain
:set cmd "sudo shutdown now"
$funcLogShutdown threshold=$threshold host=$host upsName=$upsName
$funcSshExec srcAddr=$srcAddr host=$host domain=$domain cmd=$cmd
:delay 3s
}
} else={
$funcNetworkShutdown threshold=$threshold srcAddr=$srcAddr networkDeviceArray=$networkDeviceArray mgmtDomain=$mgmtDomain host=$host domain=$domain cmd=$cmd upsName=$upsName funcLogShutdown=$funcLogShutdown funcSshExec=$funcSshExec funcNetworkShutdown=$funcNetworkShutdown
}
}
:local funcRemoteHostShutdown do={
:local host ""
:local domain ""
:local cmd ""
:set host $remoteHost
:set domain $srvDomain
:set cmd "sudo shutdown now"
:local ping [/ping src-address=$srcAddr address="$host.$domain" count=1]
:if ($ping > 0) do={
$funcLogShutdown threshold=$threshold host=$host upsName=$upsName
$funcSshExec srcAddr=$srcAddr host=$host domain=$domain cmd=$cmd
} else={
$funcLocalHostShutdown threshold=$threshold srcAddr=$srcAddr networkDeviceArray=$networkDeviceArray srvDomain=$srvDomain mgmtDomain=$mgmtDomain host=$host domain=$domain cmd=$cmd upsName=$upsName localHostArray=$localHostArray funcLogShutdown=$funcLogShutdown funcSshExec=$funcSshExec funcNetworkShutdown=$funcNetworkShutdown
}
}
:local funcSshExec do={
/system ssh-exec src-address=$srcAddr address="$host.$domain" command=$cmd
}
:local funcLogShutdown do={
:log warning "A/C power interrupted - UPS ($upsName on $[/system identity get name]) is on auxiliary power\n - Time remaining is <= to $threshold"
:log info " - Initiating $host shutdown on $[/system clock get date] at $[/system clock get time] ($[/system clock get time-zone-name])"
}
:local funcLogPowerLoss do={
:log warning "A/C power interrupted - UPS ($upsName on $[/system identity get name]) is on auxiliary power"
:log info " - Time remaining: $upsRuntime\n - Current load: $upsLoad %"
}
:local funcIfaceStatus do={
:local defLocalHostArray [:toarray ""]
:foreach iface in=$ifaceArray do={
:local ifaceRunning [/interface get [find name~"$iface"] value-name=running]
:local ifaceRunningName [/interface get [find name~"$iface"] value-name=name]
:local underscorePos [:find $ifaceRunningName "_"]
:if ($underscorePos != nil) do={
:set ifaceRunningName [:pick $ifaceRunningName ($underscorePos + 1) [:len $ifaceRunningName]]
}
:if ($ifaceRunning = true) do={
:set defLocalHostArray ($defLocalHostArray, $ifaceRunningName)
}
}
:return $defLocalHostArray
}
:local defLocalHostArray [$funcIfaceStatus ifaceArray=$defLocalHostIfaceArray]
:if ($defUpsOnline = false) do={
:if ($defUpsRuntime <= $defThreshold) do={
$funcRemoteHostShutdown threshold=$defThreshold srcAddr=$defSrcAddr remoteHost=$defRemoteHost networkDeviceArray=$defNetworkDeviceArray srvDomain=$defSrvDomain mgmtDomain=$defMgmtDomain host=$defHost domain=$defDomain cmd=$defCmd upsName=$defUpsName localHostArray=$defLocalHostArray funcLogShutdown=$funcLogShutdown funcSshExec=$funcSshExec funcLocalHostShutdown=$funcLocalHostShutdown funcNetworkShutdown=$funcNetworkShutdown
} else={
$funcLogPowerLoss upsName=$defUpsName upsRuntime=$defUpsRuntime upsLoad=$defUpsLoad
}
}
1 Like
optio
November 18, 2025, 6:42pm
2
Argument variables can be accessed in functions without naming, by order number, in function then you can access them with $<order_number> :
:local someFunct do={
:local var1 $1
:local var2 $2
:local var3 $3
...
}
$someFunct $var1 $var2 $var3
Function called from another function can be declared as global function so there is no need to pass it as argument, at the end of script you can declare global variables (functions) without value for cleanup if you dont wan’t to leave them in ROS environment when script is finished:
:global someFunct1 do={
...
}
:local someFunct2 do={
...
$someFunct1
}
$someFunct2
# global variables cleanup
:global someFunct1
1 Like
xrlls
November 18, 2025, 7:14pm
3
I tend to use structs to pass a large amount of variables, and then access the individual variables in the struct through the assigned key.
An example based on your code:
:local def {
"Threshold"=00:05:00;
"SrcAddr"=192.168.99.1;
"RemoteHost"="pve-node1";
"LocalHostIfaceArray"=[:toarray "ether7,ether6,ether5,ether4,ether3,ether2"];
"NetworkDeviceArray"= [:toarray "hap2,hap1,rb2,crs2,crs1"];
"SrvDomain"= "domain.local";
"MgmtDomain"= "domain.local";
"Email"= "email@example.com";
"Host"= "";
"Domain"="";
"Cmd"="";
"UpsName"= [/system/ups get [find] value-name=name];
"UpsOnline"= ([/system/ups/monitor $defUpsName once as-value]->"on-line");
"UpsRuntime"= ([/system/ups/monitor $defUpsName once as-value]->"runtime-left");
"UpsLoad"= ([/system/ups/monitor $defUpsName once as-value]->"load");
}
:put $def
:put ($def->"Email")
:put ($def->"NetworkDeviceArray"->0)
In this case you only need to pass the variable def to a function. Inside the function, the contents can be accessed through the ->operator as demonstrated above.
2 Likes
It all seems like a hoax to me , perhaps created using Artificial Deficiency , given the serious error in the script ...
DiscreetG33k:
this works for me
False , I do not trust what you wrote.
This line, when executed in a script, immediately shuts down the machine on which it's being executed...
because the command to be launched remotely isn't stored, but an action is immediately executed on the running machine...
the expected code must be like
:set cmd "/system shutdown"
1 Like
Only one call needs to be made, then the parameters are read.
It makes no sense to make three separate calls to read a different parameter each time.
[/system identity get name]
Called too many times.
Just call it once and put it in a variable...
What's the point?
Just set the value directly without using :set immediately afterward for no reason...
Amm0
November 19, 2025, 1:47am
6
I think @optio and @xrlls covered the alternative ways (i.e. use an array, or use positional args) to store static variables.
While I'm a big fan of functions, OP's script not exact easy to follow since there is no "main" that orchestrating the various functions & since the task is pretty sequential, the functions mask the actual order of operations making figuring out what it does harder, not easier.
Basically if a function is only called once, in RouterOS it is generally better to just "inline" the needed code to avoid dealing with arg passing or needing globals, IMO.
Agree with @rextended that there are other "improvements" (like making sure it's working) that I'd fix before worrying about styling of static variables.
And also bad scripting is translated to this....
funcLogShutdown=$funcLogShutdown funcSshExec=$funcSshExec funcNetworkShutdown=$funcNetworkShutdown
You notice "something" strange??? (ignoring my grammar errors...)
Amm0
November 19, 2025, 1:56am
8
Well, this is why excessive use of :local functions is problematic.
I cannot even get to reviewing the actual logic of UPS and SSH since there is so much boilerplate needed to wire up the local functions to variables. But the problem isn't how variables are stored, it's that there are too many here.
The only way to simplify it would be to write it without using functions...
and then actually see if we can put function only where they're really needed...
This script, aside from other serious errors that I'm tired of listing, is too functional
(I hope the joke is understood...)
2 Likes
optio
November 19, 2025, 5:47am
10
Initially I didn't bother to review that script properly, just saw bunch of nested functions and a lots of arguments, as @rextended already concluded it’s a AI crap, no need to waste time on this.
Thank you. This makes sense.
Thanks. No AI was used, just a novice trying to learn scripting and a friend who knows some bash. I appreciate the feedback and corrections. I’m new to RouterOS in general and scripting in general. Please, excuse the mistakes. That’s why I posted here… to get help from the much more experienced
The /system/shutdown was in fact meant to shutdown the host machine as the last step in the event of UPS on battery. I don’t understand how it’s incorrect, unless you’re referring to the format… /system/shutdown != “/system shutdown”??
Please explain how I could do this differently…. Should I just call the function if the value name is the same? Eg. “funcLogShutdown=$funcLogShutdown” is equal to “$funcLogShutdown”?
When you do this:
you execute the /system/shutdown command immediately right at that moment, then get the result of that command and put it in the $cmd variable. The command is executed on the current device running the big script, not on the remote host that will be connected to via SSH.
As a result, your router is in the process of shutting down even before it tries to establish the SSH connection, while still running a foreach. It might not even be able to iterate through all the items of $networkDeviceArray before shutting off.
You are not shutting down the remote device, but the current router. Instead of writing a "go kill yourself" insult to be sent to your worst enemy, you shot yourself while writing the letter.
The correct way is what @rextended wrote, only assign a string value to $cmd.
This string will then be transferred via SSH and will be properly executed on the remote device.
Understood. Thanks for the explanation
Script revised based on your recommendations. I would definitely appreciate any further advice
@rextended It’s much less “functional” lol
## Variables ###
:local threshold 00:05:00
:local srcAddr 192.168.99.1
:local remoteHost SRV
:local localHostIfaceArray [:toarray "ether7,ether6,ether5,ether4,ether3,ether2"]
:local networkDeviceArray [:toarray "hap2,hap1,rb2,crs2,crs1"]
:local srvDomain srv.domain.local
:local mgmtDomain mgmt.domain.local
:local linuxCmd "sudo shutdown now"
:local mtikCmd "/system shutdown"
:local upsName [/system ups get [find] value-name=name]
:local upsGetValue [/system ups monitor $upsName once as-value]
### Functions ###
:local funcLogShutdown do={
:log warning "A/C power interrupted - UPS ($name on $[/system identity get name]) is on auxiliary power\n - Time remaining is <= to $time"
:log info " - Initiating $host shutdown on $[/system clock get date] at $[/system clock get time] ($[/system clock get time-zone-name])"
}
:local funcShutdown do={
/system ssh-exec src-address=$src address="$host.$domain" command=$cmd
}
:local funcIfaceStatus do={
:local localHostArray [:toarray ""]
:foreach iface in=$ifaceArray do={
:local ifaceRunning [/interface get [find name~"$iface"] value-name=running]
:local ifaceRunningName [/interface get [find name~"$iface"] value-name=name]
:local underscorePos [:find $ifaceRunningName "_"]
:if ($underscorePos != nil) do={
:set ifaceRunningName [:pick $ifaceRunningName ($underscorePos + 1) [:len $ifaceRunningName]]
}
:if ($ifaceRunning = true) do={
:set localHostArray ($localHostArray, $ifaceRunningName)
}
}
:return $localHostArray
}
### Code ###
:local localHostArray [$funcIfaceStatus ifaceArray=$localHostIfaceArray]
:if (([$upsGetValue]->"on-line") = false) do={
:if (([$upsGetValue]->"runtime-left") <= $threshold) do={
:local ping [/ping src-address=$srcAddr address="$remoteHost.$srvDomain" count=1]
:if ($ping > 0) do={
$funcLogShutdown name=$upsName time=$threshold host=$remoteHost
$funcShutdown src=$srcAddr host=$remoteHost domain=$srvDomain cmd=$linuxCmd
} else={
:if ($localHostArray != "") do={
:foreach localHost in=$localHostArray do={
$funcLogShutdown name=$upsName time=$threshold host=$localHost
$funcShutdown src=$srcAddr host=$localHost domain=$srvDomain cmd=$linuxCmd
:delay 3s
}
} else={
:foreach networkDevice in=$networkDeviceArray do={
$funcLogShutdown name=$upsName time=$threshold host=$networkDevice
$funcShutdown src=$srcAddr host=$networkDevice domain=$mgmtDomain cmd=$mtikCmd
:delay 3s
}
:local host [/system identity get name]
$funcLogShutdown name=$upsName time=$threshold host=$host
/system/shutdown
}
}
} else={
:log warning "A/C power interrupted - UPS ($upsName on $[/system identity get name]) is on auxiliary power"
:log info " - Time remaining: $([$upsGetValue]->"runtime-left")\n - Current load: $([$upsGetValue]->"load") %"
}
}
:local threshold 00:05:00
:local srcAddr 192.168.99.1
:local remoteHost "SRV"
:local localHostIfaceArray [:toarray "ether7,ether6,ether5,ether4,ether3,ether2"]
:local networkDeviceArray [:toarray "hap2,hap1,rb2,crs2,crs1"]
:local srvDomain "srv.domain.local"
:local mgmtDomain "mgmt.domain.local"
:local linuxCmd "sudo shutdown now"
:local mtikCmd "/system shutdown"
:local idName [/system identity get name]
/system ups
:local upsName [get ([find]->0) name]
:local upsMonitor [monitor $upsName once as-value]
:local upsOnLine ($upsMonitor->"on-line")
:local upsRTLeft ($upsMonitor->"runtime-left")
:local upsLoad ($upsMonitor->"load")
/system clock
:local cDate [get date]
:local cTime [get time]
:local cTZone [get time-zone-name]
Study because I made some changes...
Awesome! Thank you so much
Also tried cleaning things up with positional variables… Rev.2
Anything else I could do to make this more readable?
Also, how do you get the colored code output on here??
## Variables ###
:local threshold 00:05:00
:local srcAddr 192.168.99.1
:local server "server"
:local nodeIfaceArray [:toarray "ether7,ether6,ether5,ether4,ether3,ether2"]
:local mtikArray [:toarray "hap2,hap1,rb2,crs2,crs1"]
:local srvDomain "srv.domain.local"
:local mgmtDomain "mgmt.domain.local"
:local linuxCmd "sudo shutdown now"
:local mtikCmd "/system shutdown"
:local idName [/system identity get name]
/system ups
:local upsName [get ([find]->0) name]
:local upsMonitor [monitor $upsName once as-value]
:local upsOnLine ($upsMonitor->"on-line")
:local upsRTLeft ($upsMonitor->"runtime-left")
:local upsLoad ($upsMonitor->"load")
/system clock
:local cDate [get date]
:local cTime [get time]
:local cTZone [get time-zone-name]
### Functions ###
:local funcLogShutdown do={
:local host $1
:local ups $2
:local threshold $3
:local id $4
:local date $5
:local time $6
:local zone $7
:log warning "A/C power interrupted - UPS ($ups on $id) is on auxiliary power"
:log warning " - Time remaining is <= to $threshold"
:log info " - Initiating $host shutdown on $date at $time ($zone)"
}
:local funcShutdown do={
:local src $1
:local host $2
:local domain $3
:local cmd $4
/system ssh-exec src-address=$src address="$host.$domain" command=$cmd
}
:local funcNodeIfaceMonitor do={
:local nodeArray [:toarray ""]
:foreach iface in=$ifaceArray do={
:local ifaceRunning [/interface get [find name~"$iface"] value-name=running]
:local ifaceName [/interface get [find name~"$iface"] value-name=name]
:local underscorePos [:find $ifaceName "_"]
:if ($underscorePos != nil) do={
:set ifaceName [:pick $ifaceName ($underscorePos + 1) [:len $ifaceName]]
}
:if ($ifaceRunning = true) do={
:set nodeArray ($nodeArray, $ifaceName)
}
}
:return $nodeArray
}
### Code ###
:local nodeArray [$funcNodeIfaceMonitor ifaceArray=$nodeIfaceArray]
:if ($upsOnLine = false) do={
:if ($upsRTLeft <= $threshold) do={
:local ping [/ping src-address=$srcAddr address="$server.$srvDomain" count=1]
:if ($ping > 0) do={
$funcLogShutdown $server $upsName $threshold $idName $cDate $cTime $cTZone
$funcShutdown $scrAddr $server $srvDomain $linuxCmd
} else={
:if ($nodeArray != "") do={
:foreach node in=$nodeArray do={
$funcLogShutdown $node $upsName $threshold $idName $cDate $cTime $cTZone
$funcShutdown $srcAddr $node $srvDomain $linuxCmd
:delay 3s
}
} else={
:foreach mtik in=$mtikArray do={
$funcLogShutdown $mtik $upsName $threshold $idName $cDate $cTime $cTZone
$funcShutdown $srcAddr $mtik $mgmtDomain $mtikCmd
:delay 3s
}
:local host $idName
$funcLogShutdown $host $upsName $threshold $idName $cDate $cTime $cTZone
/system shutdown
}
}
} else={
:log warning "A/C power interrupted - UPS ($upsName on $idName) is on auxiliary power"
:log info " - Time remaining: $upsRTLeft"
:log info " - Current load: $upsLoad %"
}
}
```
:local threshold 00:05:00
:local srcAddr 192.168.99.1
:local remoteHost "SRV"
```
=
:local threshold 00:05:00
:local srcAddr 192.168.99.1
:local remoteHost "SRV"
Amm0
November 19, 2025, 5:12pm
20
Use ```routeros to begin a quote blocks. It follows Markdown scheme, so you terminate it with a ``` at end.
Like
:put "$[/ip address print as-value]"
Unfortunately, the syntax coloring is flawed since it's based on V6 and an incomplete scheme.