diff --git a/test/case/ietf_routing/Readme.adoc b/test/case/ietf_routing/Readme.adoc index 99234e343..0a54ca879 100644 --- a/test/case/ietf_routing/Readme.adoc +++ b/test/case/ietf_routing/Readme.adoc @@ -13,3 +13,9 @@ include::ospf_unnumbered_interface/Readme.adoc[] include::ospf_multiarea/Readme.adoc[] include::ospf_bfd/Readme.adoc[] + +include::route_pref_ospf/Readme.adoc[] + +include::route_pref_dhcp/Readme.adoc[] + +include::route_pref_255/Readme.adoc[] diff --git a/test/case/ietf_routing/ietf_routing.yaml b/test/case/ietf_routing/ietf_routing.yaml index 452282052..4c70c1e71 100644 --- a/test/case/ietf_routing/ietf_routing.yaml +++ b/test/case/ietf_routing/ietf_routing.yaml @@ -13,3 +13,12 @@ - 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 + +- name: route_pref_255 + case: route_pref_255/test.py diff --git a/test/case/ietf_routing/route_pref_255/Readme.adoc b/test/case/ietf_routing/route_pref_255/Readme.adoc new file mode 100644 index 000000000..661dcf4b2 --- /dev/null +++ b/test/case/ietf_routing/route_pref_255/Readme.adoc @@ -0,0 +1,29 @@ +=== Route preference: Static Route Activation and Maximum Distance +==== Description +This test configures a device with a static route to a destination with +a moderate routing preference (254), verifying that it becomes active. +Then, the routing preference is increased to the maximum value (255), +which should prevent the route from becoming active. + +==== Topology +ifdef::topdoc[] +image::../../test/case/ietf_routing/route_pref_255/topology.svg[Route preference: Static Route Activation and Maximum Distance topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::route_pref_255/topology.svg[Route preference: Static Route Activation and Maximum Distance topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Route preference: Static Route Activation and Maximum Distance topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUTs +. Configure targets with active static route +. Verify that static route with preference 254 is active +. Update static route preference to 255 +. Verify that high-preference static route (255) does not become active + + +<<< + diff --git a/test/case/ietf_routing/route_pref_255/test.py b/test/case/ietf_routing/route_pref_255/test.py new file mode 100755 index 000000000..df949358d --- /dev/null +++ b/test/case/ietf_routing/route_pref_255/test.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +Route preference: Static Route Activation and Maximum Distance + +This test configures a device with a static route to a destination with +a moderate routing preference (254), verifying that it becomes active. +Then, the routing preference is increased to the maximum value (255), +which should prevent the route from becoming active. +""" + +import infamy +import infamy.route as route +from infamy.util import until, parallel + +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_initial(target, data, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + configure_interface(data, "192.168.10.1", 24), + configure_interface(link, "192.168.50.1", 24) + ] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.20.0/24", + "next-hop": {"next-hop-address": "192.168.50.2"}, + "route-preference": 254 + }] + } + } + } + ] + } + } + } + }) + +def config_target1_update(target): + target.put_config_dicts({ + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.20.0/24", + "next-hop": {"next-hop-address": "192.168.50.2"}, + "route-preference": 255 + }] + } + } + } + ] + } + } + } + }) + +def config_target2(target, data, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + configure_interface(data, "192.168.20.2", 24), + configure_interface(link, "192.168.50.2", 24) + ] + } + } + }) + +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("Configure targets with active static route"): + _, R1data = env.ltop.xlate("R1", "data") + _, R1link = env.ltop.xlate("R1", "link") + _, R2data = env.ltop.xlate("R2", "data") + _, R2link = env.ltop.xlate("R2", "link") + + parallel(config_target1_initial(R1, R1data, R1link), config_target2(R2, R2data, R2link)) + + with test.step("Verify that static route with preference 254 is active"): + until(lambda: route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-routing:static", active_check=True)) + + with test.step("Update static route preference to 255"): + config_target1_update(R1) + + with test.step("Verify that high-preference static route (255) does not become active"): + until(lambda: not route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-routing:static", active_check=True)) + + test.succeed() diff --git a/test/case/ietf_routing/route_pref_255/topology.dot b/test/case/ietf_routing/route_pref_255/topology.dot new file mode 100644 index 000000000..034d8a1cc --- /dev/null +++ b/test/case/ietf_routing/route_pref_255/topology.dot @@ -0,0 +1,38 @@ +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 | data2 | mgmt2 }", + pos="20,58!", + kind="controller", + ]; + + R1 + [ + label="{ mgmt | data | link } | R1", + pos="80,60!", + kind="infix", + ]; + + R2 + [ + label="{ link | data | mgmt } | R2", + pos="80,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"] +} diff --git a/test/case/ietf_routing/route_pref_255/topology.svg b/test/case/ietf_routing/route_pref_255/topology.svg new file mode 100644 index 000000000..9f51b89df --- /dev/null +++ b/test/case/ietf_routing/route_pref_255/topology.svg @@ -0,0 +1,81 @@ + + + + + + +route-preference + + + +PC + +PC + +mgmt1 + +data1 + +data2 + +mgmt2 + + + +R1 + +mgmt + +data + +link + +R1 + + + +PC:mgmt1--R1:mgmt + + + + +PC:data1--R1:data + +192.168.10.1/24 +192.168.10.11/24 + + + +R2 + +link + +data + +mgmt + +R2 + + + +PC:mgmt2--R2:mgmt + + + + +PC:data2--R2:data + +192.168.20.2/24 +192.168.20.22/24 + + + +R1:link--R2:link + +192.168.50.2/24 +192.168.50.1/24 + + + diff --git a/test/case/ietf_routing/route_pref_dhcp/Readme.adoc b/test/case/ietf_routing/route_pref_dhcp/Readme.adoc new file mode 100644 index 000000000..cfe8f4273 --- /dev/null +++ b/test/case/ietf_routing/route_pref_dhcp/Readme.adoc @@ -0,0 +1,33 @@ +=== Route preference: DHCP vs Static +==== Description +This test configures a device with both a DHCP-acquired route on a +dedicated interface and a static route to the same destination on +another interface. + +Initially, DHCP is preferred over Static. Afterwards, the static +route takes precedence by adjusting the routing preference value +to the one lower than DHCP. + +==== Topology +ifdef::topdoc[] +image::../../test/case/ietf_routing/route_pref_dhcp/topology.svg[Route preference: DHCP vs Static topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::route_pref_dhcp/topology.svg[Route preference: DHCP vs Static topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[Route preference: DHCP vs Static topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUTs +. Configure targets. Assign higher priority to the dhcp route +. Wait for DHCP and static routes +. Verify connectivity from PC:data12 to R2:lo via DHCP +. Assign higher priority to the static route +. Verify connectivity from PC:data12 to R2:lo via static route + + +<<< + diff --git a/test/case/ietf_routing/route_pref_dhcp/test.py b/test/case/ietf_routing/route_pref_dhcp/test.py new file mode 100755 index 000000000..e044715d1 --- /dev/null +++ b/test/case/ietf_routing/route_pref_dhcp/test.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Route preference: DHCP vs Static + +This test configures a device with both a DHCP-acquired route on a +dedicated interface and a static route to the same destination on +another interface. + +Initially, DHCP is preferred over Static. Afterwards, the static +route takes precedence by adjusting the routing preference value +to the one lower than DHCP. +""" + +import infamy +import infamy.route as route +import infamy.dhcp +from infamy.util import until, parallel +from infamy.netns import IsolatedMacVlans + +def configure_interface(name, ip=None, prefix_length=None, forwarding=True): + interface_config = { + "name": name, + "enabled": True, + "ipv4": { + "forwarding": forwarding, + "address": [] + } + } + if ip and prefix_length: + interface_config["ipv4"]["address"].append({"ip": ip, "prefix-length": prefix_length}) + return interface_config + +def config_target1(target, data1, data2, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + configure_interface(data1), + configure_interface(data2, "192.168.30.1", 24), + configure_interface(link, "192.168.50.1", 24) + ] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "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.2"}, + "route-preference": 120 + }] + } + } + } + ] + } + } + }, + "infix-dhcp-client": { + "dhcp-client": { + "enabled": True, + "client-if": [ + { + "if-name": data1, + "enabled": True, + "option": [ + {"name": "broadcast"}, + {"name": "dns"}, + {"name": "domain"}, + {"name": "hostname"}, + {"name": "ntpsrv"}, + {"name": "router"}, + {"name": "subnet"} + ], + "route-preference": 5 + } + ] + } + } + }) + +def config_target2(target, data, link): + 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("lo", "192.168.200.1", 32) + ] + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "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("Configure targets. Assign higher priority to the dhcp route"): + _, R1data1 = env.ltop.xlate("R1", "data1") + _, R1data2 = env.ltop.xlate("R1", "data2") + _, R1link = env.ltop.xlate("R1", "link") + _, R2data = env.ltop.xlate("R2", "data") + _, R2link = env.ltop.xlate("R2", "link") + + parallel(config_target1(R1, R1data1, R1data2, R1link), + config_target2(R2, R2data, R2link)) + + _, hport_data12 = env.ltop.xlate("PC", "data12") + ns0 = infamy.IsolatedMacVlan(hport_data12).start() + ns0.addip("192.168.30.3", prefix_length=24) + ns0.addroute("default", "192.168.30.1") + + _, hport_data11 = env.ltop.xlate("PC", "data11") + _, hport_data2 = env.ltop.xlate("PC", "data2") + ifmap={ hport_data11: "a", hport_data2: "b" } + ns1 = IsolatedMacVlans(ifmap, False).start() + + ns1.addip(ifname="b", addr="192.168.20.3", prefix_length=24) + ns1.addroute("192.168.200.1/32", "192.168.20.2") + + ns1.addip(ifname="a", addr="192.168.10.3", prefix_length=24) + ns1.runsh("ip route add default dev a") + + with infamy.dhcp.Server(netns=ns1, ip="192.168.10.1", netmask="255.255.255.0", router="192.168.10.3", iface="a"): + with test.step("Wait for DHCP and static routes"): + print("Waiting for DHCP and static routes...") + until(lambda: route.ipv4_route_exist(R1, "0.0.0.0/0", proto="ietf-routing:static", pref=5), attempts=200) + until(lambda: route.ipv4_route_exist(R1, "0.0.0.0/0", proto="ietf-routing:static", pref=120), attempts=200) + + with test.step("Verify connectivity from PC:data12 to R2:lo via DHCP"): + dhcp_route_active = route.ipv4_route_exist(R1, "0.0.0.0/0", pref=5, active_check=True) + assert dhcp_route_active, "Failed to activate DHCP route: Expected DHCP-acquired route not found." + + ns0.must_reach("192.168.200.1") + + with test.step("Assign higher priority to the static route"): + R1.put_config_dicts({ + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [ + { + "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.2"}, + "route-preference": 1 + }] + } + } + } + ] + } + } + } + }) + + with test.step("Verify connectivity from PC:data12 to R2:lo via static route"): + ns0.must_reach("192.168.200.1") + + static_route_active = route.ipv4_route_exist(R1, "0.0.0.0/0", pref=1, active_check=True) + assert static_route_active, "Static route activation failed: Verify route-preference adjustment on R1." + + test.succeed() diff --git a/test/case/ietf_routing/route_pref_dhcp/topology.dot b/test/case/ietf_routing/route_pref_dhcp/topology.dot new file mode 100644 index 000000000..8c4e3ba25 --- /dev/null +++ b/test/case/ietf_routing/route_pref_dhcp/topology.dot @@ -0,0 +1,40 @@ +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 | data11 | data12 | <> \n | data2 | mgmt2 }", + pos="20,50!", + kind="controller", + ]; + + R1 + [ + label="{ mgmt | data1 | data2 | link } | R1 \n 192.168.100.1/32 \n(lo)", + pos="60,51.8!", + kind="infix", + ]; + + R2 + [ + label="{ link | data | mgmt } | R2 \n 192.168.200.1/32 \n(lo)", + pos="60,42!", + kind="infix", + ]; + + PC:mgmt1 -- R1:mgmt [kind=mgmt, color="lightgray"] + PC:mgmt2 -- R2:mgmt [kind=mgmt, color="lightgray"] + + PC:data11 -- R1:data1 [color="black", headlabel="192.168.10.1/24", taillabel="192.168.10.3/24", fontcolor="black"] + PC:data12 -- R1:data2 [color="black", headlabel="192.168.30.1/24", taillabel="192.168.30.3/24", fontcolor="black"] + PC:data2 -- R2:data [color="black", headlabel="192.168.20.2/24", taillabel="192.168.20.3/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"] + +} diff --git a/test/case/ietf_routing/route_pref_dhcp/topology.svg b/test/case/ietf_routing/route_pref_dhcp/topology.svg new file mode 100644 index 000000000..6ebfe1ea4 --- /dev/null +++ b/test/case/ietf_routing/route_pref_dhcp/topology.svg @@ -0,0 +1,97 @@ + + + + + + +route-preference + + + +PC + +PC + +mgmt1 + +data11 + +data12 + + +data2 + +mgmt2 + + + +R1 + +mgmt + +data1 + +data2 + +link + +R1 + 192.168.100.1/32 +(lo) + + + +PC:mgmt1--R1:mgmt + + + + +PC:data11--R1:data1 + +192.168.10.1/24 +192.168.10.3/24 + + + +PC:data12--R1:data2 + +192.168.30.1/24 +192.168.30.3/24 + + + +R2 + +link + +data + +mgmt + +R2 + 192.168.200.1/32 +(lo) + + + +PC:mgmt2--R2:mgmt + + + + +PC:data2--R2:data + +192.168.20.2/24 +192.168.20.3/24 + + + +R1:link--R2:link + +192.168.50.2/24 +192.168.50.1/24 + + + 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 000000000..19311845e --- /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 000000000..b27b979f5 --- /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 000000000..7f9110f77 --- /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 000000000..f74e4ac07 --- /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 + + + diff --git a/test/infamy/dhcp.py b/test/infamy/dhcp.py index dc30a1b5a..7edaa41f8 100644 --- a/test/infamy/dhcp.py +++ b/test/infamy/dhcp.py @@ -1,21 +1,21 @@ """Start a DHCP server in the background""" import os -import subprocess -import ipaddress class Server: config_file = '/tmp/udhcpd.conf' leases_file = '/tmp/udhcpd.leases' - def __init__(self, netns, start='192.168.0.100', end='192.168.0.110', netmask='255.255.255.0', ip=None, router=None, prefix=None): + def __init__(self, netns, start='192.168.0.100', end='192.168.0.110', netmask='255.255.255.0', ip=None, router=None, prefix=None, iface="iface"): self.process = None self.netns = netns + self.iface = iface self._create_files(start, end, netmask, ip, router, prefix) def __del__(self): - print(self.config_file) + #print(self.config_file) #os.unlink(self.config_file) #os.unlink(self.leases_file) + pass def __enter__(self): self.start() @@ -33,7 +33,7 @@ def _create_files(self, start, end, netmask, ip, router, prefix): end=ip f.write(f'''# Generated by Infamy DHCP lease_file {self.leases_file} -interface iface +interface {self.iface} start {start} end {end} max_leases 1 @@ -52,7 +52,6 @@ def start(self): if not os.path.exists(self.config_file): raise Exception("Config file does not exist. Please create it first.") cmd = f"udhcpd -f {self.config_file}" - print(f"Starting: {cmd}") self.process = self.netns.popen(cmd.split(" ")) def stop(self): diff --git a/test/infamy/netns.py b/test/infamy/netns.py index 3d66ffe1e..ac646369a 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -3,7 +3,6 @@ import multiprocessing import os import random -import signal import subprocess import tempfile import time @@ -208,7 +207,7 @@ def addip(self, ifname, addr, prefix_length=24, proto="ipv4"): self.runsh(f""" set -ex - ip link set iface up + ip link set dev {ifname} up ip -{p} addr add {addr}/{prefix_length} dev {ifname} """, check=True) diff --git a/test/infamy/route.py b/test/infamy/route.py index 68f12b322..6e948f004 100644 --- a/test/infamy/route.py +++ b/test/infamy/route.py @@ -12,56 +12,49 @@ def _get_routes(target, protocol): return r.get("routes", {}).get("route", {}) return {} - -def _exist_route(target, dest, nexthop=None, ip=None, proto=None, pref=None): +def _exist_route(target, dest, nexthop=None, ip=None, proto=None, pref=None, active_check=False): routes = _get_routes(target, ip) for r in routes: # netconf presents destination-prefix, restconf prefix with model - dst = r.get("destination-prefix") or \ - r.get(f"ietf-{ip}-unicast-routing:destination-prefix") + dst = r.get("destination-prefix") or r.get(f"ietf-{ip}-unicast-routing:destination-prefix") if dst != dest: continue if proto is not None and r.get("source-protocol") != proto: - return False + continue if pref is not None and r.get("route-preference") != pref: - return False + continue if nexthop is not None: - nh = r["next-hop"] + nh = r.get("next-hop") if not nh: - return False + continue next_hop_list = nh.get("next-hop-list") if next_hop_list: - for nhl in next_hop_list["next-hop"]: - # netconf presents address, restconf prefix with - # model ietf-ipv4-unicast-routing:address - address = nhl.get("address") or \ - nhl.get(f"ietf-{ip}-unicast-routing:address") - if address == nexthop: - return True - return False + if not any(nhl.get("address") == nexthop or nhl.get(f"ietf-{ip}-unicast-routing:address") == nexthop + for nhl in next_hop_list["next-hop"]): + continue else: - # netconf presents next-hop-address, restconf prefix - # with model ietf-ipv4-unicast-routing:next-hop-address - nh_addr = nh.get("next-hop-address") or \ - nh.get(f"ietf-{ip}-unicast-routing:next-hop-address") - if nh_addr == nexthop: - return True - return False - else: - return True + nh_addr = nh.get("next-hop-address") or nh.get(f"ietf-{ip}-unicast-routing:next-hop-address") + if nh_addr != nexthop: + continue + + if active_check and "active" not in r: + continue + + return True + return False -def ipv4_route_exist(target, dest, nexthop=None, proto=None, pref=None): - return _exist_route(target, dest, nexthop=nexthop, ip="ipv4", proto=proto, pref=pref) +def ipv4_route_exist(target, dest, nexthop=None, proto=None, pref=None, active_check=False): + return _exist_route(target, dest, nexthop=nexthop, ip="ipv4", proto=proto, pref=pref, active_check=active_check) -def ipv6_route_exist(target, dest, nexthop=None, proto=None, pref=None): - return _exist_route(target, dest, nexthop=nexthop, ip="ipv6", proto=proto, pref=pref) +def ipv6_route_exist(target, dest, nexthop=None, proto=None, pref=None, active_check=False): + return _exist_route(target, dest, nexthop=nexthop, ip="ipv6", proto=proto, pref=pref, active_check=active_check) def _get_ospf_status(target):