Index to VLAN Automation Scripts in thread
This thread has grown to include a collection of scripts, posted at random points.... Since some post deal with troubleshooting/bugs/commentary. I'll try to maintain this index to the relevant "sub-topics" here.
Please note that all scripts here require at least 7.17+ or some case 7.18+ to work as-is. Some script be adapted to work on older versions. But the goal here was to have "some example" of scripting VLAN stuff. For example, in my uses cases, it's lab/automated testing — generally when needing a lot of VLANs quick. To be clear, it is NOT the goal to be a formal "script library" for VLANs in a live data center, but feel free to take-and-adapt as needed from here.
$mkvlan/$rmvlan/$catvlan... (just below here)
These script deal generally deal with "adding a VLAN" to fresh RouterOS, including mapping IP subnets to VLAN IDs in orderly fashion and adding DHCP stuff. Showing nothing adding, as well as "debugging" in $catvlan, and removal in $rmvlan. There are pretty simple example of "script functions for config" - which easily means you can use function as "macros" for more complex commands, since function can take parameter and other-wise "act like" built-in commands same approach can work for other areas than VLANs.
Hint:
Even if you don't use these scripts, please take it as my strong advice to > have some "IP addressing plan" > when using VLAN (and VRRP, etc .etc). I'm not sure this advice is well captured in the various VLAN guides.
$mktrunk & $rmtrunk...
These script functions will "tag" (or untag) bridge port with a provided [/b] & vice versa with vlan-id= too. Note, there is no "access port" script, see below, but $mkvlan already has example of how to make an "access port" by setting PVID on /interface/bridge/port, using the "real" command seems appropriate, i.e.
EX: So to make 'ether3' an access point, only the following additional command is:
/interface/bridge/port set [find interface=ether3] pvid=123 frame-types=allow-only-untagged
i.e. IMO no need to wrap something if just one command, so above makes an a bridge port an "access port", at least in 7.16+. The UI allow same change using PVID under VLAN section of dialog for a port under Bridge > Port. Prior version would also require the bridge port be marked as tagged= too, so for "setting an access port" it's may be more than one command prior to 7.16+.
Note:
There is no $lsvlan in the UNIX® naming style for this collection. This is the "missing script" since that show summary of IP address/routing/etc for all VLANs, basically the missing $lsvlan be the Layer 3 compliment to the $lsbridge's Layer 2 view. I have some version of one that I'll get around to posting, but it won't be as pretty as $lsbridge as my idea for $lsvlan is as a way to "export VLANs to Excel".
$lsbridge for "inspecting" VLAN bridging...
To use this one, please see the instructions listed in Post #33 which has more details. But here is what $lsbridge will visualize for a simple bridge on KNOT:
$mkvlan/$rmvlan/$catvlan Examples
First, these function will only work on 7.17+, and must have a /interface/bridge with vlan-filtering=yes enabled.
The "functions" below wrap operations around VLANs – including DHCP server, interface list, address-list, etc. They employ a few "scripting tricks" internally (including <%% operator ) and other newer syntax features for example (:grep, :convert, :serialize, export where, etc).
For example, to create a new VLAN:
$mkvlan
...and working VLAN should be added and you simply pvid= on a port in /interface/bridge/port
To "dump" the various config:
$catvlan
...which will do various "print" to output the running state.
And to remove a VLAN that was created using $mkvlan:
$rmvlan
It will automatically assign a unique IPv4 subnet based the PVID provided. So as part of the operation, another function name $pvid2array is used to calculate the various IP things needed to create things. It can be used separately to "calculate" unique subnets – but have some known scheme to generate the various IP prefix/address/pool/etc things. Another function $prettyprint , included in code below, is used to format the output. In all case, $pvid2array will always generated the SAME subnet from SAME PVID/vlan-id, so it's safe to call multiple times to get same data.
For example, $prettyprint [$pvid2array 18] will generate the following (which is used internally by $mkvlan):
{ "basename": "vlan18", "cidraddr": "192.168.18.1/24", "cidrnet": "192.168.18.0/24", "commenttag": "#autovlan 18", "dhcpdns": "192.168.18.1", "dhcpgw": "192.168.18.1", "dhcppool": "192.168.18.10-192.168.18.249", "ipprefix": "192.168.18", "routerip": "192.168.18.1", "vlanbridge": "bridge", "vlanid": 18 }
Internally $pvid2array generates a 172.x.y.0/24 address for PVID > 256... So picking a random vlan-id above 256, you can see the difference.
$prettyprint [$pvid2array [:rndnum from=257 to=4094]]
{ "basename": "vlan2668", "cidraddr": "172.25.108.1/24", "cidrnet": "172.25.108.0/24", "commenttag": "#autovlan 2668", "dhcpdns": "172.25.108.1", "dhcpgw": "172.25.108.1", "dhcppool": "172.25.108.10-172.25.108.249", "ipprefix": "172.25.108", "routerip": "172.25.108.1", "vlanbridge": "bridge", "vlanid": 2668 }
To use them, you need add /system/script named "autovlan" with a cut-and-paste of following code to the source= & then from Terminal run "/system/script run autovlan" which will allow $mkvlan, $catvlan, $rmvlan, $prettyprint, and $pvid2array function from the Terminal:
VLAN Script — $mkvlan & friend
# USER SETTINGS
# autovlanstyle - overrides the default IP addressing scheme,
# valid values: "bytes", "bytes10", or "split10"
:global autovlanstyle
# i.e. by adding a valid style to end of above, like this:
# :global autovlanstyle "split10"
# FUNCTIONS AND CODE
:global pvid2array do={
# process formating
:global autovlanstyle
:local schema "bytes"
:if ([:typeof $style] = "str") do={
:if ($style~"bytes|bytes10|split10") do={} else={
:error "$0 style= must be either bytes | bytes10 | split10 "
}
:set schema $style
}
:if ([:typeof $autovlanstyle] = "str") do={
:if ($autovlanstyle~"bytes|bytes10|split10") do={
:set schema $autovlanstyle
}
}
# move first argument to function, the PVID, to a variable
:local vlanid [:tonum $1]
# check it PVID is valid, if not show help and error (which exits script)
:if ([:typeof $vlanid] != "num" || $vlanid < 2 || $vlanid > 4094) do={
:error "PVID must be valid as first argument to command function"
}
# find the bridge interface ...
:local bridgeid [/interface bridge find vlan-filtering=yes]
:if ([:len $bridgeid] != 1) do={
:error "A bridge with vlan-filtering=yes is required, and there can be only one for this script."
}
# uses :convert to break pvid into array with 2 elements between 0-256
:local vlanbytes [:convert from=num to=byte-array $vlanid]
:local lowbits ($vlanbytes->0)
:local highbits ($vlanbytes->1)
# UGLY workaround for MIPSBE/other, detected when we don't get two parts from the vlan-id
:if ([:len $vlanbytes]>2) do={
:if ($vlanid > 255) do={
# even worse workaround, normalize to 8 bytes - ros wrongly trims leading 0
:if ([:len $vlanbytes]=7) do={
# make it len=8 by pre-pending a 0 - so the swap below is correct
:set vlanbytes (0,$vlanbytes)
}
# now swap the high and low bytes
:set lowbits ($vlanbytes->1)
:set highbits ($vlanbytes->0)
}
# lowbits is right if under 256
}
:local ipprefix "0.0.0"
:if ($schema = "bytes") do={
# for pvid below 257, use 192.168.<pvid>.0/24 as base IP prefix
# for others map pvid into unique /24 with 172.<lowbits+15>.<highbits>.0/24
:if ($vlanid < 256) do={
:set ipprefix "192.168.$vlanid"
} else={
:set ipprefix "172.$($lowbits + 15).$highbits"
}
}
:if ($schema = "bytes10") do={
# map pvid into unique /24 with 10.<lowbits>.<highbits>.0/24
:if ($vlanid < 256) do={
:set ipprefix "10.0.$vlanid"
} else={
:set ipprefix "10.$($lowbits+1).$highbits"
}
}
:if ($schema = "split10") do={
:if ($vlanid < 100) do={
:set ipprefix "10.0.$vlanid"
} else={
:set ipprefix "10.$[:tonum [:pick $vlanid 0 ([:len $vlanid]-2)]].$[:tonum [:pick $vlanid ([:len $vlanid]-2) [:len $vlanid]]]"
}
}
# now calculate the various "formats" of a prefix for use in other scripts
:return {
"vlanid"="$vlanid";
"basename"="vlan$vlanid";
"commenttag"="mkvlan $vlanid";
"vlanbridge"="$[/interface/bridge get $bridgeid name]";
"ipprefix"="$ipprefix";
"cidrnet"="$ipprefix.0/24";
"cidraddr"="$ipprefix.1/24";
"routerip"="$ipprefix.1";
"dhcppool"="$ipprefix.10-$ipprefix.249";
"dhcpgw"="$ipprefix.1";
"dhcpdns"="$ipprefix.1"
}
}
:global prettyprint do={
:if ([:typeof $1]="nothing") do={
:put "usage: $0 <data> - print provided <data>, including arrays, in a pretty format"
:put "example: $0 {\"num\"=1;\"str\"=\"text\";\"float\"=\"0.123\"}"
:error
}
:put [:serialize to=json options=json.pretty $1]
:return $1
}
:global mkvlan do={
:global pvid2array
:global mkvlan
:if ([:typeof [:tonum $1]]="num") do={
:global mkvlan
:return ($mkvlan <%% [$pvid2array [:tonum $1]])
}
:put "starting VLAN network creation for $cidrnet using id $vlanid ..."
:put " - adding $basename interface on $vlanbridge using vlan-id=$vlanid"
/interface vlan add vlan-id=$vlanid interface=$vlanbridge name=$basename comment=$commenttag
:put " - assigning IP address of $cidraddr for $basename"
/ip address add interface=$basename address=$cidraddr comment=$commenttag
:put " - adding IP address pool $dhcppool for DHCP"
/ip pool add name=$basename ranges=$dhcppool comment=$commenttag
:put " - adding dhcp-server $basename "
/ip dhcp-server add address-pool=$basename disabled=no interface=$basename name=$basename comment=$commenttag
:put " - adding DHCP /24 network using gateway=$dhcpgw and dns-server=$dhcpdns"
/ip dhcp-server network add address=$cidrnet gateway=$dhcpgw dns-server=$dhcpdns comment=$commenttag
:put " - add VLAN network to interface LAN list"
:if ([:len [/interface list find name=LAN]] = 1) do={
/interface list member add list=LAN interface=$basename comment=$commenttag
}
:put " - create FW address-list for VLAN network for $cidrnet"
/ip firewall address-list add list=$basename address=$cidrnet comment=$commenttag
:put " * NOTE: in 7.16+, the VLAN $vlanid is dynamically added to /interface/bridge/vlans with tagged=$vlanbridge "
:put " thus making an access port ONLY involves setting pvid=$vlanid on a /interface/bridge/port"
:put " * EX: So to make 'ether3' an access point, only the following additional command is:"
:put " /interface/bridge/port set [find interface=ether3] pvid=$vlanid frame-types=allow-only-untagged"
/log info [:put "VLAN network created for $cidrnet for vlan-id=$vlanid"]
}
:global rmvlan do={
:global pvid2array
:global rmvlan
:local tag "INVALID"
:if ([:typeof [:tonum $1]]="num") do={
:global rmvlan
:return ($rmvlan <%% [$pvid2array [:tonum $1]])
}
:if ([:typeof $comment]="str") do={
:set tag $comment
} else={
:if ([:typeof $commenttag]="str") do={
:set tag $commenttag
} else={
:error "$0 requires with an tag provided by '$0 comment=mytag' or via '($0 <%% [$pvid2array 1001]"
}
}
:put "starting VLAN network removal for comment=$tag"
:put " - remove $basename interface on $vlanbridge using vlan-id=$vlanid"
/interface vlan remove [find comment=$tag]
:put " - remove IP address of $cidraddr for $basename"
/ip address remove [find comment=$tag]
:put " - remove IP address pool $dhcppool for DHCP"
/ip pool remove [find comment=$tag]
:put " - removing dhcp-server $basename "
/ip dhcp-server remove [find comment=$tag]
:put " - remove DHCP /24 network using gateway=$dhcpgw and dns-server=$dhcpdns"
/ip dhcp-server network remove [find comment=$tag]
:put " - remove VLAN network to interface LAN list"
/interface list member remove [find comment=$tag]
:put " - create FW address-list for VLAN network for $cidrnet"
/ip firewall address-list remove [find comment=$tag]
/log info [:put "VLAN network removed for comment=$tag"]
}
:global catvlan do={
:global pvid2array
:global catvlan
:global prettyprint
:local tag "INVALID"
:local json [:toarray ""]
:if ([:typeof [:tonum $1]]="num") do={
:return [($catvlan <%% [$pvid2array [:tonum $1]])]
}
:if ([:typeof $comment]="str") do={
:set tag $comment
} else={
:if ([:typeof $commenttag]="str") do={
:set tag $commenttag
} else={
:error "$0 requires with an tag provided by '$0 comment=mytag' or via '($0 <%% [$pvid2array 1001]"
}
}
:set ($json->"/interface/vlan") [/interface vlan print detail as-value where comment=$tag]
:set ($json->"/ip/address") [/ip address print detail as-value where comment=$tag]
:set ($json->"/ip/pool") [/ip pool print detail as-value where comment=$tag]
:set ($json->"/ip/dhcp-server") [/ip dhcp-server print detail as-value where comment=$tag]
:set ($json->"/ip/dhcp-server/network") [/ip dhcp-server network print detail as-value where comment=$tag]
:set ($json->"/interface/list/member") [/interface list member print detail as-value where comment=$tag]
:set ($json->"/ip/firewall/address-list") [/ip firewall address-list print detail as-value where comment=$tag]
# This logic to use "export where" does not work, and causes wierd bug - disabling for now
#:put "VLAN network config..."
#:put ""
#[:parse ":grep pattern=\"^/\" script={/interface/vlan/export terse where comment=\"$tag\"} "];
#[:parse ":grep pattern=\"^/\" script={/ip/address/export terse where comment=\"$tag\"} "];
#[:parse ":grep pattern=\"^/\" script={/ip/pool/export terse where comment=\"$tag\"} "];
#[:parse ":grep pattern=\"^/\" script={:grep pattern=\"lease-time\" script={/ip/dhcp-server/export terse where comment=\"$tag\"}} "];
#[[:parse ":grep pattern=\"^/\" script={/ip/dhcp-server/network/export terse where comment=\"$tag\"} "]];
#[[:parse ":grep pattern=\"^/\" script={/interface/list/member/export terse where comment=\"$tag\"} "]];
#[[:parse ":grep pattern=\"^/\" script={/ip firewall address-list/export terse where comment=\"$tag\"} "]];
#:put ""
:return [$prettyprint $json]
}
So after adding the "autovlan" functions above to a /system/script...you can use them like so
/system/script/run autovlan
$mkvlan 123
output
starting VLAN network creation for 192.168.123.0/24 using id 123 ... - adding vlan123 interface on bridge using vlan-id=123 - assigning IP address of 192.168.123.1/24 for vlan123 - adding IP address pool 192.168.123.10-192.168.123.249 for DHCP - adding dhcp-server vlan123 - adding DHCP /24 network using gateway=192.168.123.1 and dns-server=192.168.123.1 - add VLAN network to interface LAN list - create FW address-list for VLAN network for 192.168.123.0/24 * NOTE: in 7.16+, the VLAN 123 is dynamically added to /interface/bridge/vlans with tagged=bridge thus making an access port ONLY involves setting pvid=123 on a /interface/bridge/port * EX: So to make 'ether3' an access point, only the following additional command is: /interface/bridge/port set [find interface=ether3] pvid=123 frame-types=allow-only-untagged VLAN network created for 192.168.123.0/24 for vlan-id=123
To confirm it created it use:
$catvlan 123
VLAN network debugging...
- /interface/vlan [ { ".id": "*138", "arp": "enabled", "arp-timeout": "auto", "comment": "mkvlan 123", "interface": "bridge", "l2mtu": 1588, "loop-protect": "default", "loop-protect-disable-time": "1970-01-01 00:05:00", "loop-protect-send-interval": "1970-01-01 00:00:05", "loop-protect-status": "off", "mac-address": "74:4D:28:38:B3:CD", "mtu": 1500, "mvrp": false, "name": "vlan123", "use-service-tag": false, "vlan-id": 123 } ] - /ip/address [ { ".id": "*120", "actual-interface": "vlan123", "address": "192.168.123.1/24", "comment": "mkvlan 123", "interface": "vlan123", "network": "192.168.123.0" } ] - /ip/pool [ { ".id": "*13", "comment": "mkvlan 123", "name": "vlan123", "ranges": [ "192.168.123.10-192.168.123.249" ] } ] - /ip/dhcp-server [ { ".id": "*F", "address-lists": [], "address-pool": "vlan123", "comment": "mkvlan 123", "interface": "vlan123", "lease-script": "", "lease-time": "1970-01-01 00:30:00", "name": "vlan123", "use-radius": "no" } ] - /ip/dhcp-server/network [ { ".id": "*F", "address": "192.168.123.0/24", "caps-manager": [], "comment": "mkvlan 123", "dhcp-option": [], "dns-server": "192.168.123.1", "gateway": [ "192.168.123.1" ], "ntp-server": [], "wins-server": [] } ] - /interface/list/member [ { ".id": "*6F", "comment": "mkvlan 123", "dynamic": false, "interface": "vlan123", "list": "LAN" } ] - /ip firewall address-list [ { ".id": "*1D", "address": "192.168.123.0/24", "comment": "mkvlan 123", "creation-time": "2025-01-24 16:31:46", "dynamic": false, "list": "vlan123" } ]
VLAN network config...
/interface vlan add comment="mkvlan 123" interface=bridge name=vlan123 vlan-id=123 /ip address add address=192.168.123.1/24 comment="mkvlan 123" disabled=no interface=vlan123 network=192.168.123.0 /ip pool add comment="mkvlan 123" name=vlan123 ranges=192.168.123.10-192.168.123.249 /ip dhcp-server add address-lists="" address-pool=vlan123 comment="mkvlan 123" disabled=no interface=vlan123 lease-script="" lease-time=30m n ame=vlan123 use-radius=no /ip dhcp-server network add address=192.168.123.0/24 caps-manager="" comment="mkvlan 123" dhcp-option="" dns-server=192.168.123.1 gateway=192 .168.123.1 !next-server ntp-server="" wins-server="" /interface list member add comment="mkvlan 123" disabled=no interface=vlan123 list=LAN /ip firewall address-list add address=192.168.123.0/24 comment="mkvlan 123" disabled=no dynamic=no list=vlan123
And to remove it,
$rmvlan 123
output
starting VLAN network removal for comment=mkvlan 123 - remove vlan123 interface on bridge using vlan-id=123 - remove IP address of 192.168.123.1/24 for vlan123 - remove IP address pool 192.168.123.10-192.168.123.249 for DHCP - removing dhcp-server vlan123 - remove DHCP /24 network using gateway=192.168.123.1 and dns-server=192.168.123.1 - remove VLAN network to interface LAN list - create FW address-list for VLAN network for 192.168.123.0/24 VLAN network removed for comment=mkvlan 123
And to confirm it's deleted another $catvlan 123 would show this after removal:
VLAN network debugging...
- /interface/vlan [] - /ip/address [] - /ip/pool [] - /ip/dhcp-server [] - /ip/dhcp-server/network [] - /interface/list/member [] - /ip firewall address-list [] VLAN network config...
Feel free to modified the scripts to customize how the VLAN gets created and/or how IP prefix/etc gets calculated, add VRRP to VLANs, etc. Assume you added the function to a /system/script named "autovlan"... if you want to EDIT the file, it really best to do that in the Terminal editor (Ctrl-O will save, Ctrl-C will exit without saving, Ctrl-A goes to begin of line, Ctrl-E goes to line end, 2x Ctrl-E goes to end of doc)
Here are some example usages:
/system/script/edit autovlan source
The reason for this recommendation is the Terminal's edit will do syntax-coloring — so error will be see. If you use winbox or webfig to edit script, no realtime checking is done (and the proportional font make it hard to read too).
Versions
v1.4 - cleanup after import, no change other than formatting
v1.3 - handle buggy [:convert to=byte-array] on MIPSBE, see post #20
v1.2 - fix logic error when autovlanstyle is unset
v1.1 - fix when running as /system/script; remove catvlan export where; new ip subnet schemes; minor cleanup
v1.0 - initial
