From ce73d28f6c442df803d2f2cd568b24356befa4c6 Mon Sep 17 00:00:00 2001 From: Jeroen van Bemmel Date: Wed, 24 Jun 2026 11:22:54 -0500 Subject: [PATCH 1/4] Fix: Validate FRR OSPF neighbors in VRFs Use VRF-scoped FRR OSPF/OSPFv3 show commands and add an IPv6 VLAN VRF-lite test covering OSPFv3 over trunks. Co-authored-by: Cursor --- netsim/validate/ospf/frr.py | 27 +++--- .../vlan/53-vlan-vrf-lite-ipv6.yml | 96 +++++++++++++++++++ 2 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml diff --git a/netsim/validate/ospf/frr.py b/netsim/validate/ospf/frr.py index 9bcb5a9f3c..9e9d3e0588 100644 --- a/netsim/validate/ospf/frr.py +++ b/netsim/validate/ospf/frr.py @@ -17,9 +17,9 @@ def show_ospf_neighbor(id: str, present: bool = True, vrf: str = 'default') -> str: try: ipaddress.IPv4Address(id) - except: - raise Exception(f'OSPF router ID {id} is not a valid IPv4 address') - return f'ip ospf ' + (f'vrf {vrf} ' if vrf != 'default' else '') + f'neighbor {id} json' + except Exception as exc: + raise Exception(f'OSPF router ID {id} is not a valid IPv4 address') from exc + return f'ip ospf vrf {vrf} neighbor {id} json' def valid_ospf_neighbor(id: str, present: bool = True, vrf: str = 'default') -> bool: _result = global_vars.get_result_dict('_result') @@ -31,7 +31,7 @@ def valid_ospf_neighbor(id: str, present: bool = True, vrf: str = 'default') -> if not present: return True raise Exception(f'There is no OSPFv2 neighbor {id}') - + n_state = _result[id][0] if not present: raise Exception(f'Unexpected OSPFv2 neighbor {id} in state {n_state.nbrState}') @@ -42,15 +42,16 @@ def valid_ospf_neighbor(id: str, present: bool = True, vrf: str = 'default') -> else: raise log.Result(exit_msg) -def show_ospf6_neighbor(id: str, **kwargs: typing.Any) -> str: +def show_ospf6_neighbor(id: str, present: bool = True, vrf: str = 'default', **kwargs: typing.Any) -> str: try: ipaddress.IPv4Address(id) - except: - raise Exception(f'OSPF router ID {id} is not a valid IPv4 address') - return f'ipv6 ospf6 neighbor {id} json' + except Exception as exc: + raise Exception(f'OSPF router ID {id} is not a valid IPv4 address') from exc + return f'ipv6 ospf6 vrf {vrf} neighbor {id} json' -def valid_ospf6_neighbor(id: str, present: bool = True) -> bool: +def valid_ospf6_neighbor(id: str, present: bool = True, vrf: str = 'default') -> bool: _result = global_vars.get_result_dict('_result') + vrf_name = '' if vrf == 'default' else f' in VRF {vrf}' n_state = None for n_idx in _result.keys(): @@ -62,18 +63,18 @@ def valid_ospf6_neighbor(id: str, present: bool = True) -> bool: if n_state is None: if not present: return True - raise Exception(f'There is no OSPFv3 neighbor {id}') + raise Exception(f'There is no OSPFv3 neighbor {id}{vrf_name}') else: if not present: - raise Exception(f'Unexpected OSPFv3 neighbor {id} in state {n_state.neighborState}') + raise Exception(f'Unexpected OSPFv3 neighbor {id}{vrf_name} in state {n_state.neighborState}') if n_state.neighborState != 'Full': - raise Exception(f'OSPFv3 neighbor {id} is in state {n_state.neighborState}') + raise Exception(f'OSPFv3 neighbor {id}{vrf_name} is in state {n_state.neighborState}') return True def show_ospf_prefix(pfx: str, **kwargs: typing.Any) -> str: - return f'ip ospf route json' + return 'ip ospf route json' def get_ospf_prefix(pfx: str, data: Box, **kwargs: typing.Any) -> typing.Optional[Box]: return data.get(pfx,None) diff --git a/tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml b/tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml new file mode 100644 index 0000000000..ba50e49f92 --- /dev/null +++ b/tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml @@ -0,0 +1,96 @@ +# A device has to support the following features to pass this test case: +# +# * Routed VLAN interfaces +# * VRFs +# * OSPFv3 in VRFs +# +--- +message: | + VRF lite implementation with IPv6 VLAN trunks + + * h1, h2, and h5 should be able to ping each other using IPv6 + * h3 and h4 should be able to ping each other using IPv6 + + Please note it might take a while for the lab to work due to + STP and OSPFv3 setup phase + +defaults.sources.extra: [ ../vrf/defaults-ipv6-only.yml, ../wait_times.yml, ../warnings.yml ] + +groups: + _auto_create: True + routers: + members: [ r1, r2, r3 ] + module: [ ospf, vrf, vlan ] + role: router + x_routers: + members: [ r2 ] + device: frr + provider: clab + hosts: + members: [ h1, h2, h3, h4, h5 ] + device: linux + provider: clab + +vrfs: + red: + blue: + +vlans: + red: + mode: route + vrf: red + links: [ r1-h1, r3-h2, r1-h5 ] + blue: + mode: route + vrf: blue + links: [ r1-h3, r3-h4 ] + +links: +- r1: + r2: + vlan.trunk: [ red, blue ] +- r2: + r3: + vlan.trunk: [ red, blue ] + +validate: + adj_r1_b: + description: Check OSPFv3 adjacencies (R2-R1) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_stp + nodes: [ r2 ] + plugin: ospf6_neighbor(nodes.r1.vrfs.blue.ospf.router_id,vrf='blue') + adj_r1_r: + description: Check OSPFv3 adjacencies (R2-R1) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_lan + nodes: [ r2 ] + plugin: ospf6_neighbor(nodes.r1.vrfs.red.ospf.router_id,vrf='red') + adj_r3_b: + description: Check OSPFv3 adjacencies (R2-R3) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_lan + nodes: [ r2 ] + plugin: ospf6_neighbor(nodes.r3.vrfs.blue.ospf.router_id,vrf='blue') + adj_r3_r: + description: Check OSPFv3 adjacencies (R2-R3) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_lan + nodes: [ r2 ] + plugin: ospf6_neighbor(nodes.r3.vrfs.red.ospf.router_id,vrf='red') + red: + description: Intra-VLAN reachability (red) + wait_msg: Waiting for STP forwarding state + wait: ping_stp + nodes: [ h1, h2 ] + plugin: ping('h5',af='ipv6') + blue: + description: Intra-VLAN reachability (blue) + nodes: [ h3 ] + wait: ping_long + devices: [ linux ] + plugin: ping('h4',af='ipv6') + filter: + description: Inter-VLAN isolation (red - blue) + nodes: [ h1, h2 ] + plugin: ping('h3',af='ipv6',expect='fail') From 001b164ad6d9909fd0bc87b5150188727f34a6a0 Mon Sep 17 00:00:00 2001 From: Jeroen van Bemmel Date: Wed, 24 Jun 2026 11:46:52 -0500 Subject: [PATCH 2/4] Fix: Clean up FRR OSPF validation helpers Remove stale validator code and avoid unnecessary f-strings in static FRR OSPF commands. Co-authored-by: Cursor --- netsim/validate/ospf/frr.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/netsim/validate/ospf/frr.py b/netsim/validate/ospf/frr.py index 9e9d3e0588..eb3e8f0fb0 100644 --- a/netsim/validate/ospf/frr.py +++ b/netsim/validate/ospf/frr.py @@ -64,7 +64,7 @@ def valid_ospf6_neighbor(id: str, present: bool = True, vrf: str = 'default') -> if not present: return True raise Exception(f'There is no OSPFv3 neighbor {id}{vrf_name}') - else: + else: if not present: raise Exception(f'Unexpected OSPFv3 neighbor {id}{vrf_name} in state {n_state.neighborState}') @@ -109,7 +109,7 @@ def valid_ospf_prefix( names = OSPF_PREFIX_NAMES) def show_ospf6_prefix(pfx: str, **kwargs: typing.Any) -> str: - return f'ipv6 ospf6 route detail json' + return 'ipv6 ospf6 route detail json' def get_ospf6_prefix(pfx: str, data: Box) -> typing.Optional[Box]: return data.get('routes').get(pfx,None) @@ -123,8 +123,6 @@ def check_ospf6_cost(data: list, value: typing.Any, **kwargs: typing.Any) -> lis return m_value - return [ p for p in data if p.metricCost == value ] - def check_ospf6_rt(data: list, value: typing.Any, **kwargs: typing.Any) -> list: return [ p for p in data if p.pathType == value ] @@ -158,10 +156,10 @@ def valid_ipv6_route( if not _result: raise Exception(f'The routing table has no {proto} routes') - + if not pfx in _result: raise Exception(f'The prefix {pfx} is not in the routing table or not a {proto} route') - + pfx_data = _result[pfx][0] if cost is not None and cost != pfx_data.metric: raise Exception(f'Invalid OSPF end-to-end cost for prefix {pfx}: expected {cost} actual {pfx_data.metric}') From 0ff71595ff514d6bc285e6000be8540ef59e6dd1 Mon Sep 17 00:00:00 2001 From: Jeroen van Bemmel Date: Wed, 24 Jun 2026 13:12:48 -0500 Subject: [PATCH 3/4] Fix: Merge VLAN VRF-lite IPv6 test Fold the IPv6 OSPFv3 VLAN VRF-lite coverage into the existing test case and remove the duplicate numbered topology. Co-authored-by: Cursor --- tests/integration/vlan/52-vlan-vrf-lite.yml | 50 +++++----- .../vlan/53-vlan-vrf-lite-ipv6.yml | 96 ------------------- 2 files changed, 25 insertions(+), 121 deletions(-) delete mode 100644 tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml diff --git a/tests/integration/vlan/52-vlan-vrf-lite.yml b/tests/integration/vlan/52-vlan-vrf-lite.yml index ebd101fa8b..ba50e49f92 100644 --- a/tests/integration/vlan/52-vlan-vrf-lite.yml +++ b/tests/integration/vlan/52-vlan-vrf-lite.yml @@ -2,19 +2,19 @@ # # * Routed VLAN interfaces # * VRFs -# * OSPF in VRFs +# * OSPFv3 in VRFs # --- message: | - VRF lite implementation with VLAN trunks + VRF lite implementation with IPv6 VLAN trunks - * h1, h2, and h5 should be able to ping each other - * h3 and h4 should be able to ping each other + * h1, h2, and h5 should be able to ping each other using IPv6 + * h3 and h4 should be able to ping each other using IPv6 Please note it might take a while for the lab to work due to - STP and OSPF setup phase + STP and OSPFv3 setup phase -defaults.sources.extra: [ ../wait_times.yml, ../warnings.yml ] +defaults.sources.extra: [ ../vrf/defaults-ipv6-only.yml, ../wait_times.yml, ../warnings.yml ] groups: _auto_create: True @@ -55,42 +55,42 @@ links: validate: adj_r1_b: - description: Check OSPF adjacencies (R2-R1) - wait_msg: Waiting for OSPF adjacencies to form - wait: ospfv2_adj_stp + description: Check OSPFv3 adjacencies (R2-R1) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_stp nodes: [ r2 ] - plugin: ospf_neighbor(nodes.r1.vrfs.blue.ospf.router_id,vrf='blue') + plugin: ospf6_neighbor(nodes.r1.vrfs.blue.ospf.router_id,vrf='blue') adj_r1_r: - description: Check OSPF adjacencies (R2-R1) - wait_msg: Waiting for OSPF adjacencies to form - wait: ospfv2_adj_lan + description: Check OSPFv3 adjacencies (R2-R1) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_lan nodes: [ r2 ] - plugin: ospf_neighbor(nodes.r1.vrfs.red.ospf.router_id,vrf='red') + plugin: ospf6_neighbor(nodes.r1.vrfs.red.ospf.router_id,vrf='red') adj_r3_b: - description: Check OSPF adjacencies (R2-R3) - wait_msg: Waiting for OSPF adjacencies to form - wait: ospfv2_adj_lan + description: Check OSPFv3 adjacencies (R2-R3) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_lan nodes: [ r2 ] - plugin: ospf_neighbor(nodes.r3.vrfs.blue.ospf.router_id,vrf='blue') + plugin: ospf6_neighbor(nodes.r3.vrfs.blue.ospf.router_id,vrf='blue') adj_r3_r: - description: Check OSPF adjacencies (R2-R3) - wait_msg: Waiting for OSPF adjacencies to form - wait: ospfv2_adj_lan + description: Check OSPFv3 adjacencies (R2-R3) + wait_msg: Waiting for OSPFv3 adjacencies to form + wait: ospfv3_adj_lan nodes: [ r2 ] - plugin: ospf_neighbor(nodes.r3.vrfs.red.ospf.router_id,vrf='red') + plugin: ospf6_neighbor(nodes.r3.vrfs.red.ospf.router_id,vrf='red') red: description: Intra-VLAN reachability (red) wait_msg: Waiting for STP forwarding state wait: ping_stp nodes: [ h1, h2 ] - plugin: ping('h5') + plugin: ping('h5',af='ipv6') blue: description: Intra-VLAN reachability (blue) nodes: [ h3 ] wait: ping_long devices: [ linux ] - plugin: ping('h4') + plugin: ping('h4',af='ipv6') filter: description: Inter-VLAN isolation (red - blue) nodes: [ h1, h2 ] - plugin: ping('h3',expect='fail') + plugin: ping('h3',af='ipv6',expect='fail') diff --git a/tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml b/tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml deleted file mode 100644 index ba50e49f92..0000000000 --- a/tests/integration/vlan/53-vlan-vrf-lite-ipv6.yml +++ /dev/null @@ -1,96 +0,0 @@ -# A device has to support the following features to pass this test case: -# -# * Routed VLAN interfaces -# * VRFs -# * OSPFv3 in VRFs -# ---- -message: | - VRF lite implementation with IPv6 VLAN trunks - - * h1, h2, and h5 should be able to ping each other using IPv6 - * h3 and h4 should be able to ping each other using IPv6 - - Please note it might take a while for the lab to work due to - STP and OSPFv3 setup phase - -defaults.sources.extra: [ ../vrf/defaults-ipv6-only.yml, ../wait_times.yml, ../warnings.yml ] - -groups: - _auto_create: True - routers: - members: [ r1, r2, r3 ] - module: [ ospf, vrf, vlan ] - role: router - x_routers: - members: [ r2 ] - device: frr - provider: clab - hosts: - members: [ h1, h2, h3, h4, h5 ] - device: linux - provider: clab - -vrfs: - red: - blue: - -vlans: - red: - mode: route - vrf: red - links: [ r1-h1, r3-h2, r1-h5 ] - blue: - mode: route - vrf: blue - links: [ r1-h3, r3-h4 ] - -links: -- r1: - r2: - vlan.trunk: [ red, blue ] -- r2: - r3: - vlan.trunk: [ red, blue ] - -validate: - adj_r1_b: - description: Check OSPFv3 adjacencies (R2-R1) - wait_msg: Waiting for OSPFv3 adjacencies to form - wait: ospfv3_adj_stp - nodes: [ r2 ] - plugin: ospf6_neighbor(nodes.r1.vrfs.blue.ospf.router_id,vrf='blue') - adj_r1_r: - description: Check OSPFv3 adjacencies (R2-R1) - wait_msg: Waiting for OSPFv3 adjacencies to form - wait: ospfv3_adj_lan - nodes: [ r2 ] - plugin: ospf6_neighbor(nodes.r1.vrfs.red.ospf.router_id,vrf='red') - adj_r3_b: - description: Check OSPFv3 adjacencies (R2-R3) - wait_msg: Waiting for OSPFv3 adjacencies to form - wait: ospfv3_adj_lan - nodes: [ r2 ] - plugin: ospf6_neighbor(nodes.r3.vrfs.blue.ospf.router_id,vrf='blue') - adj_r3_r: - description: Check OSPFv3 adjacencies (R2-R3) - wait_msg: Waiting for OSPFv3 adjacencies to form - wait: ospfv3_adj_lan - nodes: [ r2 ] - plugin: ospf6_neighbor(nodes.r3.vrfs.red.ospf.router_id,vrf='red') - red: - description: Intra-VLAN reachability (red) - wait_msg: Waiting for STP forwarding state - wait: ping_stp - nodes: [ h1, h2 ] - plugin: ping('h5',af='ipv6') - blue: - description: Intra-VLAN reachability (blue) - nodes: [ h3 ] - wait: ping_long - devices: [ linux ] - plugin: ping('h4',af='ipv6') - filter: - description: Inter-VLAN isolation (red - blue) - nodes: [ h1, h2 ] - plugin: ping('h3',af='ipv6',expect='fail') From 15caa9479a2d9632886bd2928d20228b8bf641c0 Mon Sep 17 00:00:00 2001 From: Jeroen van Bemmel Date: Wed, 24 Jun 2026 13:16:46 -0500 Subject: [PATCH 4/4] Fix: Add OSPFv2 checks to VLAN VRF-lite test Validate IPv4 OSPFv2 adjacency and reachability alongside the existing IPv6 OSPFv3 VLAN VRF-lite coverage. Co-authored-by: Cursor --- tests/integration/vlan/52-vlan-vrf-lite.yml | 70 ++++++++++++++++----- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/tests/integration/vlan/52-vlan-vrf-lite.yml b/tests/integration/vlan/52-vlan-vrf-lite.yml index ba50e49f92..dd65aabfd1 100644 --- a/tests/integration/vlan/52-vlan-vrf-lite.yml +++ b/tests/integration/vlan/52-vlan-vrf-lite.yml @@ -2,17 +2,17 @@ # # * Routed VLAN interfaces # * VRFs -# * OSPFv3 in VRFs +# * OSPF and OSPFv3 in VRFs # --- message: | - VRF lite implementation with IPv6 VLAN trunks + VRF lite implementation with dual-stack VLAN trunks - * h1, h2, and h5 should be able to ping each other using IPv6 - * h3 and h4 should be able to ping each other using IPv6 + * h1, h2, and h5 should be able to ping each other using IPv4 and IPv6 + * h3 and h4 should be able to ping each other using IPv4 and IPv6 Please note it might take a while for the lab to work due to - STP and OSPFv3 setup phase + STP, OSPF, and OSPFv3 setup phase defaults.sources.extra: [ ../vrf/defaults-ipv6-only.yml, ../wait_times.yml, ../warnings.yml ] @@ -54,43 +54,83 @@ links: vlan.trunk: [ red, blue ] validate: - adj_r1_b: + adj_v2_r1_b: + description: Check OSPFv2 adjacencies (R2-R1) + wait_msg: Waiting for OSPFv2 adjacencies to form + wait: ospfv2_adj_stp + nodes: [ r2 ] + plugin: ospf_neighbor(nodes.r1.vrfs.blue.ospf.router_id,vrf='blue') + adj_v2_r1_r: + description: Check OSPFv2 adjacencies (R2-R1) + wait_msg: Waiting for OSPFv2 adjacencies to form + wait: ospfv2_adj_lan + nodes: [ r2 ] + plugin: ospf_neighbor(nodes.r1.vrfs.red.ospf.router_id,vrf='red') + adj_v2_r3_b: + description: Check OSPFv2 adjacencies (R2-R3) + wait_msg: Waiting for OSPFv2 adjacencies to form + wait: ospfv2_adj_lan + nodes: [ r2 ] + plugin: ospf_neighbor(nodes.r3.vrfs.blue.ospf.router_id,vrf='blue') + adj_v2_r3_r: + description: Check OSPFv2 adjacencies (R2-R3) + wait_msg: Waiting for OSPFv2 adjacencies to form + wait: ospfv2_adj_lan + nodes: [ r2 ] + plugin: ospf_neighbor(nodes.r3.vrfs.red.ospf.router_id,vrf='red') + adj_v3_r1_b: description: Check OSPFv3 adjacencies (R2-R1) wait_msg: Waiting for OSPFv3 adjacencies to form wait: ospfv3_adj_stp nodes: [ r2 ] plugin: ospf6_neighbor(nodes.r1.vrfs.blue.ospf.router_id,vrf='blue') - adj_r1_r: + adj_v3_r1_r: description: Check OSPFv3 adjacencies (R2-R1) wait_msg: Waiting for OSPFv3 adjacencies to form wait: ospfv3_adj_lan nodes: [ r2 ] plugin: ospf6_neighbor(nodes.r1.vrfs.red.ospf.router_id,vrf='red') - adj_r3_b: + adj_v3_r3_b: description: Check OSPFv3 adjacencies (R2-R3) wait_msg: Waiting for OSPFv3 adjacencies to form wait: ospfv3_adj_lan nodes: [ r2 ] plugin: ospf6_neighbor(nodes.r3.vrfs.blue.ospf.router_id,vrf='blue') - adj_r3_r: + adj_v3_r3_r: description: Check OSPFv3 adjacencies (R2-R3) wait_msg: Waiting for OSPFv3 adjacencies to form wait: ospfv3_adj_lan nodes: [ r2 ] plugin: ospf6_neighbor(nodes.r3.vrfs.red.ospf.router_id,vrf='red') - red: - description: Intra-VLAN reachability (red) + red_v4: + description: IPv4 intra-VLAN reachability (red) + wait_msg: Waiting for STP forwarding state + wait: ping_stp + nodes: [ h1, h2 ] + plugin: ping('h5',af='ipv4') + red_v6: + description: IPv6 intra-VLAN reachability (red) wait_msg: Waiting for STP forwarding state wait: ping_stp nodes: [ h1, h2 ] plugin: ping('h5',af='ipv6') - blue: - description: Intra-VLAN reachability (blue) + blue_v4: + description: IPv4 intra-VLAN reachability (blue) + nodes: [ h3 ] + wait: ping_long + devices: [ linux ] + plugin: ping('h4',af='ipv4') + blue_v6: + description: IPv6 intra-VLAN reachability (blue) nodes: [ h3 ] wait: ping_long devices: [ linux ] plugin: ping('h4',af='ipv6') - filter: - description: Inter-VLAN isolation (red - blue) + filter_v4: + description: IPv4 inter-VLAN isolation (red - blue) + nodes: [ h1, h2 ] + plugin: ping('h3',af='ipv4',expect='fail') + filter_v6: + description: IPv6 inter-VLAN isolation (red - blue) nodes: [ h1, h2 ] plugin: ping('h3',af='ipv6',expect='fail')