ROS Scripting question

# Define settings for interfaces
:local interfaceConfigs {
    {"P_NEW_WG_3_NS2"; 52821; "2"; "interfacekey1"; "TX100"; "10.3.0.0"; "10.3.0.2"; "TX100"; "ip1"; "10.3.0.1"; "10.7.0.1"; "peerkey1"};
    {"P_NEW_WG_4_NS0"; 52822; "0"; "interfacekey2"; "CA200"; "10.4.0.0"; "10.4.0.2"; "CA200"; "ip2"; "10.4.0.1"; "10.3.0.1"; "peerkey2"};
    {"P_NEW_WG_5_NS1"; 52823; "1"; "interfacekey3"; "GA100"; "10.5.0.0"; "10.5.0.2"; "GA100"; "ip3"; "10.5.0.1"; "10.6.0.1"; "peerkey3"};
    {"P_NEW_WG_6_NS1"; 52824; "1"; "interfacekey4"; "NY600"; "10.6.0.0"; "10.6.0.2"; "NY600"; "ip4"; "10.6.0.1"; "10.5.0.1"; "peerkey4"};
    {"P_NEW_WG_7_NS2"; 52825; "2"; "interfacekey5"; "CO261"; "10.7.0.0"; "10.7.0.2"; "CO261"; "ip5"; "10.7.0.1"; "10.4.0.1"; "peerkey5"}
}

:local networkprefix "/30"
:local protonid "PROTON"

# Remove existing configurations with the same comment
/interface wireguard remove [find comment~"^$protonid"]
/ip address remove [find comment~"^$protonid"]
/ip firewall mangle remove [find comment~"^$protonid"]
/ip route remove [find comment~"^$protonid"]
/ip dhcp-server network remove [find comment~"^$protonid"]
/interface wireguard peers remove [find comment~"^$protonid"]
/interface list member remove [find comment~"^$protonid"]
/routing rule remove [find comment~"^$protonid"]

# Loop through interface configurations
:local count [len $interfaceConfigs]
:for i from=0 to=($count - 1) do={
    :local config $interfaceConfigs->[$i]
    :local currentInterfaceName [:pick $config 0]
    :local currentListenPort [:pick $config 1]
    :local currentNetshieldValue [:pick $config 2]
    :local currentPrivateKey [:pick $config 3]
    :local currentServer [:pick $config 4]
    :local currentNetworkID [:pick $config 5]
    :local currentInterfaceAddress [:pick $config 6]
    :local currentPeerServer [:pick $config 7]
    :local currentEndpoint [:pick $config 8]
    :local currentGateway [:pick $$config 9]
    :local currentReversedGateway [:pick $$config 10]
    :local currentPublicKey [:pick $config 11]

    # Add WireGuard interface
    /interface wireguard add comment=("$protonid " . $currentNetworkID . " Netshield " . $currentNetshieldValue . " " . $currentServer) listen-port=$currentListenPort mtu=1420 \
        name=$currentInterfaceName private-key=$currentPrivateKey

    # Add IP address for the interface
    /ip address add address=($currentInterfaceAddress . $networkprefix) comment=$protonid interface=$currentInterfaceName network=$currentNetworkID

    # Add mangle rule
    /ip firewall mangle add action=change-mss chain=forward comment=$protonid new-mss=1360 \
        out-interface=$currentInterfaceName passthrough=yes protocol=tcp tcp-flags=syn \
        tcp-mss=!0-1375

    # Add route proto_combo routing table with distance based on index
    /ip route add check-gateway=ping dst-address=0.0.0.0/0 gateway=$currentGateway distance=($i + 1) comment=("$protonid Combo Route for " . $currentGateway . " Distance " . ($i + 1)) routing-table=proton_combo

    # Add route to the proton_combo_reversed routing table with distance based on index
    /ip route add check-gateway=ping dst-address=0.0.0.0/0 gateway=$currentReversedGateway distance=($i + 1) comment=("$protonid Combo Reversed Route for " . $currentReversedGateway . " Distance " . ($i + 1)) routing-table=proton_combo_reversed

    # Add DHCP server network
    /ip dhcp-server network add address=($currentNetworkID . $networkprefix) comment=("$protonid Network for " . $currentNetworkID) dns-server=$currentGateway gateway=$currentInterfaceAddress

    # Add WireGuard peers
    /interface wireguard peers add interface=$currentInterfaceName public-key=$currentPublicKey allowed-address=0.0.0.0/0 endpoint-address=$currentEndpoint name=$currentPeerServer endpoint-port=51820 persistent-keepalive=25s \
        comment=("$protonid Peer " . $currentPeerServer)

    # Add WAN members
    /interface list member add comment=$protonid interface=$currentInterfaceName list=WAN

    # Add routing rules
    /routing rule add action=lookup-only-in-table table=main src-address=0.0.0.0/0 dst-address=($currentNetworkID . $networkprefix) place-before=4 comment=("$protonid Routing Rule for " . $currentNetworkID)
}

there seems to be syntax issue here. :local config $interfaceConfigs->[$i] . can someone help? manual mentions this as the way to access 2D Arrays

It’s a list of lists, so indexes are just the numbers, wrapped in (). So…
($interfaceConfigs->0->1)
would be 52821

Notes:

  • Do not use ($interfaceConfig->“1”) as that will not work in the case, plain numbers for a list. The ($array->“1”) syntax - with quotes - is for map arrays like {“1”=“first-val”;“2”=“second”}.
  • The accessors for NOT for arrays - they create for a subshell (like the in bash).
  • Arrays are 0-based, with rows first, then columns. You can do that instead of the [:pick].

Thanks Amm0. the easy solve without much code change was to change to
:local config [:pick $interfaceConfigs $i] and everything works
understand your point of lists vs name-value pairs. will keep in mind next time

The scripting, and the concepts behind it, are way beyond me, but I am curious what this script does?

What problem does it solve?

OP uses an array to define what config to later do wrt to interfaces. Mikrotik @dru has a video on arrays here: https://www.youtube.com/watch?v=eWCJw0uZ-lE

To summarize, the key is the array at the top & I suppose the OP change the array on a per router basis to define wireguard peers to add:

# Define settings for interfaces
:local interfaceConfigs {
    {"P_NEW_WG_3_NS2"; 52821; "2"; "interfacekey1"; "TX100"; "10.3.0.0"; "10.3.0.2"; "TX100"; "ip1"; "10.3.0.1"; "10.7.0.1"; "peerkey1"};
    {"P_NEW_WG_4_NS0"; 52822; "0"; "interfacekey2"; "CA200"; "10.4.0.0"; "10.4.0.2"; "CA200"; "ip2"; "10.4.0.1"; "10.3.0.1"; "peerkey2"};
    {"P_NEW_WG_5_NS1"; 52823; "1"; "interfacekey3"; "GA100"; "10.5.0.0"; "10.5.0.2"; "GA100"; "ip3"; "10.5.0.1"; "10.6.0.1"; "peerkey3"};
    {"P_NEW_WG_6_NS1"; 52824; "1"; "interfacekey4"; "NY600"; "10.6.0.0"; "10.6.0.2"; "NY600"; "ip4"; "10.6.0.1"; "10.5.0.1"; "peerkey4"};
    {"P_NEW_WG_7_NS2"; 52825; "2"; "interfacekey5"; "CO261"; "10.7.0.0"; "10.7.0.2"; "CO261"; "ip5"; "10.7.0.1"; "10.4.0.1"; "peerkey5"}
}

and OP used a “:for” loop later that use that array list to to do the actual configuration. Since the array is a list (no attributes) is why the OP needs to use the -># sytnax, not ->“#”, and the syntax for array is just wrong. And it for the “:for” was converted to a “:foreach” it might be a little clear:

:foreach i in=$interfaceConfigs do={
    :local config $i
    :local currentInterfaceName ($i->0)
    :local currentListenPort ($i->1)
    :local currentNetshieldValue ($i->2)
    :local currentPrivateKey ($i->3)
    :local currentServer ($i->4)
    :local currentNetworkID ($i->5)
    :local currentInterfaceAddress ($i->6)
    :local currentPeerServer($i->7)
    :local currentEndpoint($i->8)
    :local currentGateway ($i->9)
    :local currentReversedGateway ($i->10)
    :local currentPublicKey ($i->11)

    # Add WireGuard interface
    /interface wireguard add comment=("$protonid " . $currentNetworkID . " Netshield " . $currentNetshieldValue . " " . $currentServer) listen-port=$currentListenPort mtu=1420 \
        name=$currentInterfaceName private-key=$currentPrivateKey

    # ...
}

Thanks Amm0.

So (in simple language), the script can be run on a group of routers and:

  1. Removes any wireguard interfaces, ip addresses, firewall mangle fules, ip routes, dhcp-servers, wireguard peers, interface lists, and routing rules that have “PROTON” in the comment

  2. Adds new entries for all those config items using the values in the array “interfaceConfigs”

And because the array does not have keys (I hope that’s the right term), the values in the array are referenced by their position using:

$i->#

as in:

:local currentInterfaceName ($i->0)

Correct?

That is beyond elegant!

Thank you!

Yup. Let me provide a quick example that you can try… And show using “named” array elements too…

One thing that help to build the array to use is [:deserialize] - this is pretty new so you need “stable” (or perhaps 7.15 dunno). And… you need some CSV - for example, an export from an Excel/etc sheet. If the CSV looked like this:

name,subnet,vlan
office,10.10.1.0/24,10
guest,192.168.20.0/24,20

… we can make it a RouterOS array to use with the “fun” … $var-># or $var->“attribute” things.

Now to get that into an array, you can use “:deserialize from=dsv” to read the CSV data – if it was called “excel.csv” in the root or RouterOS Files, you’d do this:
:global csvstr [/file get excel.csv content]

But to demo, I put the CSV into a string - to show how this works without real files - but be rest be same with file:

“csvstr” could be read from file, using string for demo

:global csvstr “name,subnet,vlan\noffice,10.10.1.1/24,10\nguest,192.168.20.1/24,20”

and it can be read in TWO ways into an array - check the options=

:global dsvarray [:deserialize $csvstr from=dsv delimiter=“,” options=dsv.array]
:global dsvplain [:deserialize $csvstr from=dsv delimiter=“,” options=dsv.plain]

now $dsvarray and $dsvplain can be used to get the hang of using an array

… if you can use excel/etc … this skips the create a routeros array class initially :wink:# using the “dsv.plain” - form: $var->#->#

:put ($dsvplain->0->0)

name

:put ($dsvplain->1->1)

10.10.1.1/24

just note that variable types are converted… even ip-prefix:

:put [:typeof ($dsvplain->1->1)]

ip-prefex

using “dsv.array” is likely better in MOST cases

with that option=, the array form changes to: $var->#->“column-name”

… with the first row is used as “attributes” in the array for each row

this form is in “dsvarray” var above.

to use it for something useful like “creating vlans” be just:

:foreach row in=$dsvarray do= {
/interface/vlan/add name=($row->“name”) vlan-id=[:tonum ($row->“vlan”)] interface=bridge comment=“dsvarray”
}

which get created & print shows:

/interface/vlan/print where comment=“dsvarray”

NAME MTU ARP VLAN-ID INTERFACE

;;; dsvarray

0 R guest 1500 enabled 820 bridge

;;; dsvarray

1 R office 1500 enabled 810 bridge

and to remove, use same “trick” to look at the comment (that was added in the foreach when created):

/interface/vlan remove [find comment=dsvarray]

and same print will return nothin…

/interface/vlan/print where comment=“dsvarray”
Note: Beyond :deserialize…the 2nd trick here is the :foreach – that what make “going through” / iterating over “rows” possible/easier. And obviously the “config part” here is light for example.

This could be a VLAN or WG/peers or address-list or whatever… define them in an Excel/Sheets/Numbers/etc first… then export as CSV, and copy .csv file to RouterOS. You can use the “Excel data” in any command you want. The code be shorter than above since I show TWO way of using :deserialize. But the 2nd way with"option=dsv.array" version is likely more clear in scripts for CSV.