From af2b7480bf8a6dccb5a5d5becd10b13b8ef3c1ae Mon Sep 17 00:00:00 2001 From: Aleksey Popov Date: Fri, 5 Jun 2026 13:44:44 +0000 Subject: [PATCH] Add OSPFv3 (IPv6 OSPF) support for FortiOS The netlab FortiOS OSPF template supported only OSPFv2, but the device manifest declared neither ospf.ipv4 nor ospf.ipv6, so _routing.py auto-enabled ospf.af.ipv6 on any node that had IPv6 OSPF interfaces. The single, address-family-agnostic ospf/fortios.j2 then ran its IPv4-only body against those interfaces and failed in ansible.utils.ipaddr ('width' is undefined) the moment it formatted intf.ipv4 on an IPv6-only link. This left IPv6 iBGP labs, which need an IPv6 IGP to carry loopback reachability, with no usable FortiOS IGP. Render both OSPF instances from a single ospf_config(proto, intf_list, ospf) macro, invoked once per active address family. The caller pre-filters the interface list by address family and passes the macro only the interfaces that belong in that instance, so address-family correctness is established once at the boundary rather than re-checked throughout the body. proto ('ospf' or 'ospf6') is both the FortiOS configuration keyword and the discriminator for the few places the two instances diverge. Three constraints are not apparent from the diff: - The OSPFv2 interface list must contain only interfaces that have an IPv4 address. FortiOS binds an interface to its area through a prefix in `config network`, which requires the interface's IPv4 subnet; OSPFv3 instead binds the area with `set area-id` inside `config ospf6-interface`. - `config area` is emitted before the interface stanza, because FortiOS rejects `set area-id` for an area that has not yet been defined ("Please input a defined area id!"). - OSPFv3 authentication on FortiOS is IPsec-based; plaintext and MD5 keys do not apply, so the password line is emitted for OSPFv2 only. No device-manifest change is required: AF support defaults to enabled unless the device manifest disables it. Co-Authored-By: Claude Opus 4.8 --- docs/module/ospf.md | 2 +- docs/platforms.md | 1 + netsim/ansible/templates/ospf/fortios.j2 | 65 +++++++++++++++--------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/module/ospf.md b/docs/module/ospf.md index e19d3e8b53..3484bbcb8b 100644 --- a/docs/module/ospf.md +++ b/docs/module/ospf.md @@ -63,7 +63,7 @@ The following table describes the per-platform support of individual router-leve | Dell OS10 ([❗](caveats-os10)) | ✅| ✅| ✅| ✅| ✅| | Cumulus Linux 5.x (NVUE) | ✅| ✅| ❌ | ✅ | ✅| | Extreme Networks EXOS | ✅| ✅| ✅| ❌ | ❌ | -| Fortinet FortiOS |[❗](caveats-fortios)| ✅ | ❌ | ❌ | ❌ | +| Fortinet FortiOS |[❗](caveats-fortios)| ✅ | ✅ | ❌ | ❌ | | FRR | ✅| ✅| ✅| ✅| ✅| | Junos[^Junos] | ✅| ✅| ✅| ✅| ❌ | | Mikrotik RouterOS 6 | ✅| ❌ | ❌ | ❌ | ❌ | diff --git a/docs/platforms.md b/docs/platforms.md index 21c03226c6..4094d930fb 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -523,6 +523,7 @@ Core *netlab* functionality and all multi-protocol routing protocol configuratio | Cumulus Linux NVUE | ❌ | ❌ | ❌ | ✅ | ❌ | | Dell OS10 | ✅ | ❌ | ❌ | ✅ | ❌ | | Extreme Networks EXOS | ✅ | ❌ | ❌ | ❌ | ❌ | +| Fortinet FortiOS | ✅ | ❌ | ❌ | ❌ | ❌ | | FRR | ✅ | ✅ | ❌ | ✅ | ❌ | | Junos[^Junos] | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | | Mikrotik RouterOS 6 | ❌ | ❌ | ❌ | ✅ | ❌ | diff --git a/netsim/ansible/templates/ospf/fortios.j2 b/netsim/ansible/templates/ospf/fortios.j2 index d4ac1a8c04..489254a8a0 100644 --- a/netsim/ansible/templates/ospf/fortios.j2 +++ b/netsim/ansible/templates/ospf/fortios.j2 @@ -1,26 +1,37 @@ {% set vdom_traffic = netlab_vdom|default(vdom) %} {% set multi_vdom = vdom_traffic != vdom %} - -{% if multi_vdom %} -config vdom - edit {{ vdom_traffic }} -{% endif %} - -config router ospf +{# + Shared OSPFv2/OSPFv3 renderer. 'proto' is the FortiOS render token ('ospf' or 'ospf6') and + doubles as the address-family fork discriminator. 'intf_list' is pre-filtered by the caller to + the interfaces that run OSPF on this AF, so the macro never sees an off-AF interface. +#} +{% macro ospf_config(proto, intf_list, ospf) %} +{% set intf_stanza = proto ~ '-interface' %} +{% set passive_intfs = intf_list|selectattr('ospf.passive', 'defined')|selectattr('ospf.passive')|map(attribute='ifname')|list %} +config router {{ proto }} set router-id {{ ospf.router_id }} {% if ospf.reference_bandwidth is defined %} set auto-cost-ref-bandwidth {{ ospf.reference_bandwidth }} {% endif %} -{% set passive_intfs = netlab_interfaces|selectattr('ospf', 'defined')|selectattr('ospf.passive', 'defined')|selectattr('ospf.passive')|map(attribute='ifname')|list %} {% if passive_intfs|length > 0 %} set passive-interface {{ passive_intfs|join(' ') }} {% endif %} -{% if netlab_interfaces|selectattr('ospf', 'defined')|list|length > 0 %} - config ospf-interface -{% for intf in netlab_interfaces if 'ospf' in intf %} + config area +{% for area_data in ospf.areas|default([]) %} + edit {{ area_data.area }} + next +{% endfor %} + end +{% if intf_list|length > 0 %} + + config {{ intf_stanza }} +{% for intf in intf_list %} edit "{{ intf.ifname }}" set interface "{{ intf.ifname }}" +{% if proto == 'ospf6' %} + set area-id {{ intf.ospf.area }} +{% endif %} {% if intf.ospf.network_type is defined %} set network-type {{ intf.ospf.network_type }} {% endif %} @@ -36,33 +47,41 @@ config router ospf {% if intf.ospf.priority is defined %} set priority {{ intf.ospf.priority }} {% endif %} -{% if intf.ospf.password is defined %} +{% if proto == 'ospf' and intf.ospf.password is defined %} set authentication text set authentication-key {{ intf.ospf.password }} {% endif %} set status enable next -{% endfor %} +{% endfor %} end {% endif %} - - config area -{% for area_data in ospf.areas|default([]) %} - edit {{ area_data.area }} - next -{% endfor %} - end +{% if proto == 'ospf' %} config network -{% for intf in netlab_interfaces if intf.ospf.area is defined %} +{% for intf in intf_list if intf.ospf.area is defined %} edit {{ loop.index }} set prefix {{ intf.ipv4|ansible.utils.ipaddr('subnet') }} set area {{ intf.ospf.area }} next -{% endfor %} +{% endfor %} end +{% endif %} end - +{% endmacro %} +{% if multi_vdom %} +config vdom + edit {{ vdom_traffic }} +{% endif %} +{% set ospf_intf = netlab_interfaces|selectattr('ospf', 'defined')|list %} +{% set v4 = ospf_intf|selectattr('ipv4', 'defined')|list %} +{% set v6 = ospf_intf|selectattr('ipv6', 'defined')|list %} +{% if ospf.af.ipv4 is defined %} +{{ ospf_config('ospf', v4, ospf) }} +{% endif %} +{% if ospf.af.ipv6 is defined %} +{{ ospf_config('ospf6', v6, ospf) }} +{% endif %} {% if multi_vdom %} end {% endif %}