diff --git a/test/case/ietf_routing/Readme.adoc b/test/case/ietf_routing/Readme.adoc index 99234e34..937ec7c8 100644 --- a/test/case/ietf_routing/Readme.adoc +++ b/test/case/ietf_routing/Readme.adoc @@ -13,3 +13,5 @@ include::ospf_unnumbered_interface/Readme.adoc[] include::ospf_multiarea/Readme.adoc[] include::ospf_bfd/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 45228205..1a3c8f8f 100644 --- a/test/case/ietf_routing/ietf_routing.yaml +++ b/test/case/ietf_routing/ietf_routing.yaml @@ -13,3 +13,6 @@ - name: ospf_bfd case: ospf_bfd/test.py + +- name: route_pref_dhcp + case: route_pref_dhcp/test.py 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 00000000..cfe8f427 --- /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 00000000..e044715d --- /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 00000000..8c4e3ba2 --- /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 00000000..6ebfe1ea --- /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 + + +