test: new test, dhcp hostname priority test

Verify deterministic hostname management via /etc/hostname.d/, rules:

 1. Configured hostname takes precedence over default
 2. DHCP hostname takes precedence over configured
 3. Hostname reverts when DHCP lease ends

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-11-20 10:45:43 +01:00
parent 0d44b44bac
commit df1df83229
7 changed files with 166 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,26 @@
=== DHCP Hostname Priority
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_dhcp/client_hostname]
==== Description
Verify deterministic hostname management: a DHCP acquired hostname takes
precedence over a configured hostname. When a DHCP lease ends, or the
hostname option is removed, the system should revert to the configured
hostname.
==== Topology
image::topology.svg[DHCP Hostname Priority topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Configure static system hostname
. Verify configured hostname is set
. Enable DHCP client requesting hostname option
. Verify DHCP hostname takes precedence
. Drop hostname option from client request
. Verify hostname reverts to configured value
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""DHCP Hostname Priority
Verify deterministic hostname management: a DHCP acquired hostname takes
precedence over a configured hostname. When a DHCP lease ends, or the
hostname option is removed, the system should revert to the configured
hostname.
"""
import infamy, infamy.dhcp
from infamy.util import until
def verify_hostname(node, expected):
"""Verify operational hostname matches expected value"""
data = node.get_data("/ietf-system:system")
return data["system"]["hostname"] == expected
with infamy.Test() as test:
DHCP_HOSTNAME = "dhcp-assigned"
CONF_HOSTNAME = "configured-host"
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
client = env.attach("client", "mgmt")
_, host = env.ltop.xlate("host", "mgmt")
_, port = env.ltop.xlate("client", "mgmt")
with test.step("Configure static system hostname"):
client.put_config_dict("ietf-system", {
"system": {
"hostname": CONF_HOSTNAME
}
})
with test.step("Verify configured hostname is set"):
until(lambda: verify_hostname(client, CONF_HOSTNAME))
with infamy.IsolatedMacVlan(host, mode="private") as netns:
netns.addip("10.0.0.1")
with infamy.dhcp.Server(netns, ip="10.0.0.42", hostname=DHCP_HOSTNAME):
with test.step("Enable DHCP client requesting hostname option"):
client.put_config_dict("ietf-interfaces", {
"interfaces": {
"interface": [{
"name": port,
"ipv4": {
"infix-dhcp-client:dhcp": {
"option": [
{"id": "vendor-class", "value": "infamy"},
{"id": "hostname"},
{"id": "netmask"},
{"id": "router"}
]
}
}
}]
}
})
with test.step("Verify DHCP hostname takes precedence"):
until(lambda: verify_hostname(client, DHCP_HOSTNAME))
with test.step("Drop hostname option from client request"):
path = f"/ietf-interfaces:interfaces/interface[name='{port}']" \
+ "/ietf-ip:ipv4/infix-dhcp-client:dhcp/option[id='hostname']"
client.delete_xpath(path)
with test.step("Verify hostname reverts to configured value"):
until(lambda: verify_hostname(client, CONF_HOSTNAME))
test.succeed()
@@ -0,0 +1,22 @@
graph "1x1" {
layout="neato";
overlap="false";
esep="+100";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt }",
pos="0,20!",
requires="controller",
];
client [
label="{ <mgmt> mgmt } | client",
pos="200,20!",
requires="infix",
];
host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey]
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="474pt" height="45pt"
viewBox="0.00 0.00 474.04 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 470.04,-41 470.04,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- client -->
<g id="node2" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="350.04,-0.5 350.04,-36.5 466.04,-36.5 466.04,-0.5 350.04,-0.5"/>
<text text-anchor="middle" x="375.04" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="400.04,-0.5 400.04,-36.5 "/>
<text text-anchor="middle" x="433.04" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-18.5C100,-18.5 350.04,-18.5 350.04,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -11,6 +11,9 @@
- name: DHCP option 121 vs option 3
case: client_routes/test.py
- name: DHCP Hostname Priority
case: client_hostname/test.py
- name: DHCPv6 Basic
case: client6_basic/test.py
+7 -3
View File
@@ -8,11 +8,13 @@ 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, iface="iface"):
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,
hostname=None, iface="iface"):
self.process = None
self.netns = netns
self.iface = iface
self._create_files(start, end, netmask, ip, router, prefix)
self._create_files(start, end, netmask, ip, router, prefix, hostname)
def __del__(self):
#print(self.config_file)
@@ -26,7 +28,7 @@ class Server:
def __exit__(self, _, __, ___):
self.stop()
def _create_files(self, start, end, netmask, ip, router, prefix):
def _create_files(self, start, end, netmask, ip, router, prefix, hostname):
f = open(self.leases_file, "w")
f.close()
@@ -47,6 +49,8 @@ option lease 864000
f.write(f"option router {router}\n")
if prefix and router:
f.write(f"option staticroutes {prefix} {router}\n")
if hostname:
f.write(f"option hostname {hostname}\n")
def get_pid(self):
return self.process.pid