diff --git a/test/case/infix_dhcp/client_hostname/Readme.adoc b/test/case/infix_dhcp/client_hostname/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/infix_dhcp/client_hostname/test.adoc b/test/case/infix_dhcp/client_hostname/test.adoc new file mode 100644 index 00000000..55756a6c --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/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 + + diff --git a/test/case/infix_dhcp/client_hostname/test.py b/test/case/infix_dhcp/client_hostname/test.py new file mode 100755 index 00000000..afdadee5 --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/test.py @@ -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() diff --git a/test/case/infix_dhcp/client_hostname/topology.dot b/test/case/infix_dhcp/client_hostname/topology.dot new file mode 100644 index 00000000..d2b1b2bc --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/topology.dot @@ -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 }", + pos="0,20!", + requires="controller", + ]; + + client [ + label="{ mgmt } | client", + pos="200,20!", + requires="infix", + ]; + + host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey] +} diff --git a/test/case/infix_dhcp/client_hostname/topology.svg b/test/case/infix_dhcp/client_hostname/topology.svg new file mode 100644 index 00000000..20a632e9 --- /dev/null +++ b/test/case/infix_dhcp/client_hostname/topology.svg @@ -0,0 +1,33 @@ + + + + + + +1x1 + + + +host + +host + +mgmt + + + +client + +mgmt + +client + + + +host:mgmt--client:mgmt + + + + diff --git a/test/case/infix_dhcp/dhcp_client.yaml b/test/case/infix_dhcp/dhcp_client.yaml index fc9dd57e..b6ac309d 100644 --- a/test/case/infix_dhcp/dhcp_client.yaml +++ b/test/case/infix_dhcp/dhcp_client.yaml @@ -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 diff --git a/test/infamy/dhcp.py b/test/infamy/dhcp.py index 01158b9e..9a3aacd1 100644 --- a/test/infamy/dhcp.py +++ b/test/infamy/dhcp.py @@ -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