2026-06-07 08:34:13 +02:00
|
|
|
---
|
2026-06-08 19:39:04 +02:00
|
|
|
# VLAN-aware bridge, access/trunk ports, and the management VLAN interface.
|
|
|
|
|
#
|
|
|
|
|
# ORDERING IS DELIBERATE (lockout safety): bridge (filtering OFF) -> ports+pvid ->
|
|
|
|
|
# VLAN membership -> mgmt VLAN iface + IP -> default route -> vlan-filtering LAST.
|
|
|
|
|
# Enabling vlan-filtering is the point at which a wrong management path strands the
|
|
|
|
|
# switch, so it runs only after the mgmt VLAN/IP exist. Keep a serial/WinBox-MAC
|
|
|
|
|
# recovery channel open when running this against a live device.
|
|
|
|
|
#
|
|
|
|
|
# DEFCONF NOTE: on a factory-default CRS310 the `bridge` already exists with every
|
|
|
|
|
# port as an untagged member and the management IP sits directly on `bridge`
|
|
|
|
|
# (192.168.88.1/24). This role does NOT delete that legacy IP — after you have
|
|
|
|
|
# proven reachability on the new mgmt VLAN, remove the old bridge IP on-site so the
|
|
|
|
|
# device is reachable only via vlan-mgmt. The guards below adopt the existing bridge
|
|
|
|
|
# and ports rather than recreating them.
|
|
|
|
|
#
|
|
|
|
|
# Idempotency comes from the RouterOS `:if [find]` guards (changed_when: false).
|
|
|
|
|
|
|
|
|
|
- name: Create VLAN-aware bridge (filtering off initially)
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/interface/bridge/find name="{{ switch_bridge_name }}"]] = 0)
|
|
|
|
|
do={ /interface/bridge/add name="{{ switch_bridge_name }}"
|
|
|
|
|
vlan-filtering=no }
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Add or adopt bridge ports and set their PVID
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/interface/bridge/port/find interface="{{ item.interface }}"]] = 0)
|
|
|
|
|
do={ /interface/bridge/port/add bridge="{{ switch_bridge_name }}"
|
|
|
|
|
interface="{{ item.interface }}" pvid={{ item.pvid }} }
|
|
|
|
|
else={ /interface/bridge/port/set [find interface="{{ item.interface }}"]
|
|
|
|
|
pvid={{ item.pvid }} }
|
|
|
|
|
loop: "{{ switch_bridge_ports }}"
|
|
|
|
|
loop_control:
|
|
|
|
|
label: "{{ item.interface }} (pvid {{ item.pvid }})"
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
# tagged = trunk ports whose tagged_vlans include this id, plus the bridge (CPU)
|
|
|
|
|
# ONLY on the management VLAN so the vlan-mgmt interface is reachable.
|
|
|
|
|
# untagged = access ports whose pvid equals this id.
|
|
|
|
|
- name: Define bridge VLANs (tagged/untagged membership)
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:local tagged "{{ ((switch_bridge_ports
|
|
|
|
|
| selectattr('mode', 'equalto', 'trunk')
|
|
|
|
|
| selectattr('tagged_vlans', 'defined')
|
|
|
|
|
| selectattr('tagged_vlans', 'contains', item.id)
|
|
|
|
|
| map(attribute='interface') | list)
|
|
|
|
|
+ ([switch_bridge_name] if item.id == switch_mgmt_vlan_id else []))
|
|
|
|
|
| join(',') }}";
|
|
|
|
|
:local untagged "{{ switch_bridge_ports
|
|
|
|
|
| selectattr('mode', 'equalto', 'access')
|
|
|
|
|
| selectattr('pvid', 'equalto', item.id)
|
|
|
|
|
| map(attribute='interface') | list | join(',') }}";
|
|
|
|
|
:if ([:len [/interface/bridge/vlan/find vlan-ids={{ item.id }}]] = 0)
|
|
|
|
|
do={ /interface/bridge/vlan/add bridge="{{ switch_bridge_name }}"
|
|
|
|
|
vlan-ids={{ item.id }} tagged=$tagged untagged=$untagged }
|
|
|
|
|
else={ /interface/bridge/vlan/set [find vlan-ids={{ item.id }}]
|
|
|
|
|
tagged=$tagged untagged=$untagged }
|
|
|
|
|
loop: "{{ switch_vlans }}"
|
|
|
|
|
loop_control:
|
|
|
|
|
label: "vlan {{ item.id }} ({{ item.name }})"
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Create the management VLAN interface
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/interface/vlan/find name="vlan-mgmt"]] = 0)
|
|
|
|
|
do={ /interface/vlan/add name="vlan-mgmt"
|
|
|
|
|
interface="{{ switch_bridge_name }}" vlan-id={{ switch_mgmt_vlan_id }} }
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
2026-06-09 12:15:23 +02:00
|
|
|
# On a defconf switch the mgmt IP lives directly on the bare `bridge`; it must move to
|
|
|
|
|
# vlan-mgmt (same address can't be on both). Removing it drops a session reaching the
|
|
|
|
|
# switch THROUGH the bridge, so during the first cutover this is done out-of-band as one
|
|
|
|
|
# detached flip (see the design doc's runbook), not by a straight play run. In steady
|
|
|
|
|
# state both tasks below are no-ops.
|
|
|
|
|
- name: Remove the legacy management IP from the bare bridge interface
|
2026-06-09 12:38:04 +02:00
|
|
|
# NOTE: RouterOS `find ... address=<v>` does NOT match an ip/address prefix value
|
|
|
|
|
# (it returns 0 even on an exact string), so match by get-and-compare instead.
|
2026-06-09 12:15:23 +02:00
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
2026-06-09 12:38:04 +02:00
|
|
|
:foreach a in=[/ip/address/find interface="{{ switch_bridge_name }}"]
|
|
|
|
|
do={ :if ([/ip/address/get $a address]="{{ switch_mgmt_address }}")
|
|
|
|
|
do={ /ip/address/remove $a } }
|
2026-06-09 12:15:23 +02:00
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Assign the management IP address to vlan-mgmt
|
2026-06-08 19:39:04 +02:00
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/ip/address/find interface="vlan-mgmt"]] = 0)
|
|
|
|
|
do={ /ip/address/add address="{{ switch_mgmt_address }}"
|
|
|
|
|
interface="vlan-mgmt" }
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
2026-06-09 12:55:03 +02:00
|
|
|
# Optional DHCP server on the isolated mgmt VLAN: plug into the mgmt port and get an
|
|
|
|
|
# address automatically (login to the switch is still required). Guards by name; the
|
|
|
|
|
# network entry guards on "no networks yet" because RouterOS find-by-address does not
|
|
|
|
|
# match prefix values (see the legacy-bridge-IP note above).
|
|
|
|
|
- name: Create the management DHCP address pool
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/ip/pool/find name="mgmt-pool"]] = 0)
|
|
|
|
|
do={ /ip/pool/add name="mgmt-pool" ranges="{{ switch_mgmt_dhcp_pool }}" }
|
|
|
|
|
when: switch_mgmt_dhcp_enabled | bool
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Create the management DHCP server on vlan-mgmt
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/ip/dhcp-server/find name="mgmt-dhcp"]] = 0)
|
|
|
|
|
do={ /ip/dhcp-server/add name="mgmt-dhcp" interface="vlan-mgmt"
|
|
|
|
|
address-pool="mgmt-pool" lease-time=1h disabled=no }
|
|
|
|
|
when: switch_mgmt_dhcp_enabled | bool
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Define the management DHCP network
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/ip/dhcp-server/network/find]] = 0)
|
|
|
|
|
do={ /ip/dhcp-server/network/add address="{{ switch_mgmt_dhcp_network }}"
|
|
|
|
|
gateway="{{ switch_mgmt_address.split('/')[0] }}" }
|
|
|
|
|
when: switch_mgmt_dhcp_enabled | bool
|
|
|
|
|
changed_when: false
|
|
|
|
|
|
2026-06-08 19:39:04 +02:00
|
|
|
- name: Set the default gateway route
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- >-
|
|
|
|
|
:if ([:len [/ip/route/find dst-address="0.0.0.0/0"]] = 0)
|
|
|
|
|
do={ /ip/route/add dst-address=0.0.0.0/0
|
|
|
|
|
gateway="{{ switch_mgmt_gateway }}" }
|
2026-06-09 12:15:23 +02:00
|
|
|
when: switch_mgmt_gateway | length > 0
|
2026-06-08 19:39:04 +02:00
|
|
|
changed_when: false
|
|
|
|
|
|
|
|
|
|
- name: Enable VLAN filtering (LAST — prove mgmt reachability first)
|
|
|
|
|
community.routeros.command:
|
|
|
|
|
commands:
|
|
|
|
|
- /interface/bridge/set "{{ switch_bridge_name }}" vlan-filtering=yes
|
|
|
|
|
changed_when: false
|