--- # 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 - name: Assign the management IP address 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 - 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 }}" } 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