diff --git a/test/case/ietf_routing/Readme.adoc b/test/case/ietf_routing/Readme.adoc index 937ec7c8..afc73832 100644 --- a/test/case/ietf_routing/Readme.adoc +++ b/test/case/ietf_routing/Readme.adoc @@ -14,4 +14,6 @@ include::ospf_multiarea/Readme.adoc[] include::ospf_bfd/Readme.adoc[] +include::route_pref_ospf/Readme.adoc[] + include::route_pref_dhcp/Readme.adoc[] diff --git a/test/case/ietf_routing/ietf_routing.yaml b/test/case/ietf_routing/ietf_routing.yaml index 1a3c8f8f..00f59493 100644 --- a/test/case/ietf_routing/ietf_routing.yaml +++ b/test/case/ietf_routing/ietf_routing.yaml @@ -13,6 +13,9 @@ - name: ospf_bfd case: ospf_bfd/test.py + +- name: route_pref_ospf + case: route_pref_ospf/test.py - name: route_pref_dhcp case: route_pref_dhcp/test.py diff --git a/test/case/ietf_routing/route_pref_ospf/Readme.adoc b/test/case/ietf_routing/route_pref_ospf/Readme.adoc new file mode 100644 index 00000000..19311845 --- /dev/null +++ b/test/case/ietf_routing/route_pref_ospf/Readme.adoc @@ -0,0 +1,35 @@ +=== Route preference: OSPF vs Static +==== Description +This test configures a device with both an OSPF-acquired route on a +dedicated interface and a static route to the same destination on +another interface. The static route has a higher preference value than +OSPF. + +Initially, the device should prefer the OSPF route; if the OSPF route +becomes unavailable, the static route should take over. + +==== Topology +ifdef::topdoc[] +image::../../test/case/ietf_routing/route_pref_ospf/topology.svg[Route preference: OSPF vs Static topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::route_pref_ospf/topology.svg[Route preference: OSPF vs Static topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Route preference: OSPF vs Static topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUTs +. Set up TPMR between R1ospf and R2ospf +. Configure targets +. Set up persistent MacVlan namespaces +. Wait for OSPF and static routes +. Verify connectivity from PC:data1 to PC:data2 via OSPF +. Simulate OSPF route loss by blocking OSPF interface +. Verify connectivity via static route after OSPF failover + + +<<< + diff --git a/test/case/ietf_routing/route_pref_ospf/test.py b/test/case/ietf_routing/route_pref_ospf/test.py new file mode 100755 index 00000000..b27b979f --- /dev/null +++ b/test/case/ietf_routing/route_pref_ospf/test.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Route preference: OSPF vs Static + +This test configures a device with both an OSPF-acquired route on a +dedicated interface and a static route to the same destination on +another interface. The static route has a higher preference value than +OSPF. + +Initially, the device should prefer the OSPF route; if the OSPF route +becomes unavailable, the static route should take over. +""" + +import infamy +import infamy.route as route +from infamy.util import until, parallel +from infamy.netns import TPMR + + +def configure_interface(name, ip, prefix_length, forwarding=True): + return { + "name": name, + "enabled": True, + "ipv4": { + "forwarding": forwarding, + "address": [{"ip": ip, "prefix-length": prefix_length}] + } + } + +def config_target1(target, data, link, ospf): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + configure_interface(data, "192.168.10.1", 24), + configure_interface(link, "192.168.50.1", 24), + configure_interface(ospf, "192.168.60.1", 24) + ] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "type": "infix-routing:ospfv2", + "name": "ospf-default", + "ospf": { + "redistribute": { + "redistribute": [{"protocol": "connected"}] + }, + "areas": { + "area": [{ + "area-id": "0.0.0.0", + "interfaces": { + "interface": [{ + "name": ospf, + "hello-interval": 1, + "dead-interval": 3 + }] + } + }] + } + } + }, + { + "type": "infix-routing:static", + "name": "dot20", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.20.0/24", + "next-hop": {"next-hop-address": "192.168.50.2"}, + "route-preference": 120 + }] + } + } + } + ] + } + } + } + }) + +def config_target2(target, data, link, ospf): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + configure_interface(data, "192.168.20.2", 24), + configure_interface(link, "192.168.50.2", 24), + configure_interface(ospf, "192.168.60.2", 24) + ] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "type": "infix-routing:ospfv2", + "name": "ospf-default", + "ospf": { + "redistribute": { + "redistribute": [{"protocol": "connected"}] + }, + "areas": { + "area": [{ + "area-id": "0.0.0.0", + "interfaces": { + "interface": [{ + "name": ospf, + "hello-interval": 1, + "dead-interval": 3 + }] + } + }] + } + } + }, + { + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "0.0.0.0/0", + "next-hop": {"next-hop-address": "192.168.50.1"} + }] + } + } + } + ] + } + } + } + }) + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUTs"): + env = infamy.Env() + R1 = env.attach("R1", "mgmt") + R2 = env.attach("R2", "mgmt") + + with test.step("Set up TPMR between R1ospf and R2ospf"): + ospf_breaker = TPMR(env.ltop.xlate("PC", "R1_ospf")[1], env.ltop.xlate("PC", "R2_ospf")[1]).start() + + with test.step("Configure targets"): + _, R1data = env.ltop.xlate("R1", "data") + _, R1link = env.ltop.xlate("R1", "link") + _, R1ospf = env.ltop.xlate("R1", "ospf") + _, R2data = env.ltop.xlate("R2", "data") + _, R2link = env.ltop.xlate("R2", "link") + _, R2ospf = env.ltop.xlate("R2", "ospf") + + parallel(config_target1(R1, R1data, R1link, R1ospf), config_target2(R2, R2data, R2link, R2ospf)) + + with test.step("Set up persistent MacVlan namespaces"): + _, hport_data1 = env.ltop.xlate("PC", "data1") + _, hport_data2 = env.ltop.xlate("PC", "data2") + + ns1 = infamy.IsolatedMacVlan(hport_data1).start() + ns1.addip("192.168.10.11", prefix_length=24) + ns1.addroute("default", "192.168.10.1") + + ns2 = infamy.IsolatedMacVlan(hport_data2).start() + ns2.addip("192.168.20.22", prefix_length=24) + ns2.addroute("default", "192.168.20.2") + + with test.step("Wait for OSPF and static routes"): + print("Waiting for OSPF and static routes...") + until(lambda: route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-ospf:ospfv2"), attempts=200) + until(lambda: route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-routing:static"), attempts=200) + + with test.step("Verify connectivity from PC:data1 to PC:data2 via OSPF"): + ns1.must_reach("192.168.20.22") + + ospf_route_active = route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-ospf:ospfv2", active_check=True) + assert ospf_route_active, "OSPF route should be preferred when available." + + hops = [row[1] for row in ns1.traceroute("192.168.20.22")] + assert "192.168.60.2" in hops, f"Path does not use expected OSPF route: {hops}" + + with test.step("Simulate OSPF route loss by blocking OSPF interface"): + ospf_breaker.block() + until(lambda: not route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-ospf:ospfv2"), attempts=200) + + with test.step("Verify connectivity via static route after OSPF failover"): + ns1.must_reach("192.168.20.22") + + static_route_active = route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-routing:static", active_check=True) + assert static_route_active, "Static route should be preferred when OSPF route is unavailable." + + hops = [row[1] for row in ns1.traceroute("192.168.20.22")] + assert "192.168.50.2" in hops, f"Path does not use expected static route: {hops}" + + test.succeed() diff --git a/test/case/ietf_routing/route_pref_ospf/topology.dot b/test/case/ietf_routing/route_pref_ospf/topology.dot new file mode 100644 index 00000000..7f9110f7 --- /dev/null +++ b/test/case/ietf_routing/route_pref_ospf/topology.dot @@ -0,0 +1,41 @@ +graph "route-preference" { + layout="neato"; + overlap="false"; + esep="+20"; + size=10 + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + PC + [ + label="PC | { mgmt1 | data1 | <> \n\n | R1_ospf | R2_ospf | <> \n\n | data2 | mgmt2 }", + pos="20,50!", + kind="controller", + ]; + + R1 + [ + label="{ mgmt | data | ospf | link } | R1", + pos="70,58!", + kind="infix", + ]; + + R2 + [ + label="{ link | ospf | data | mgmt } | R2", + pos="70,42!", + kind="infix", + ]; + + PC:mgmt1 -- R1:mgmt [kind=mgmt, color="lightgray"] + PC:mgmt2 -- R2:mgmt [kind=mgmt, color="lightgray"] + + PC:data1 -- R1:data [color="black", headlabel="192.168.10.1/24", taillabel="192.168.10.11/24", fontcolor="black"] + PC:data2 -- R2:data [color="black", headlabel="192.168.20.2/24", taillabel="192.168.20.22/24", fontcolor="black"] + + R1:link -- R2:link [headlabel="192.168.50.2/24", taillabel="192.168.50.1/24", labeldistance=1, fontcolor="black", color="black"] + + R1:ospf -- PC:R1_ospf [color="lightgreen", taillabel="192.168.60.1/24"] + R2:ospf -- PC:R2_ospf [color="lightgreen", taillabel="192.168.60.2/24"] +} diff --git a/test/case/ietf_routing/route_pref_ospf/topology.svg b/test/case/ietf_routing/route_pref_ospf/topology.svg new file mode 100644 index 00000000..f74e4ac0 --- /dev/null +++ b/test/case/ietf_routing/route_pref_ospf/topology.svg @@ -0,0 +1,103 @@ + + + + + + +route-preference + + + +PC + +PC + +mgmt1 + +data1 + + +R1_ospf + +R2_ospf + + +data2 + +mgmt2 + + + +R1 + +mgmt + +data + +ospf + +link + +R1 + + + +PC:mgmt1--R1:mgmt + + + + +PC:data1--R1:data + +192.168.10.1/24 +192.168.10.11/24 + + + +R2 + +link + +ospf + +data + +mgmt + +R2 + + + +PC:mgmt2--R2:mgmt + + + + +PC:data2--R2:data + +192.168.20.2/24 +192.168.20.22/24 + + + +R1:ospf--PC:R1_ospf + +192.168.60.1/24 + + + +R1:link--R2:link + +192.168.50.2/24 +192.168.50.1/24 + + + +R2:ospf--PC:R2_ospf + +192.168.60.2/24 + + +