diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 9f79193fb..0dda4878a 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -23,6 +23,7 @@ All notable changes to the project are documented in this file. `92.2` instead of `92.246.137.39` - Fix #1466: `show container` showing no output for containers whose command line includes environment variables +- Fix #1439: changing hostname does not regenerate DHCP client conf until restart [BPI-R4]: https://docs.banana-pi.org/en/BPI-R4/BananaPi_BPI-R4 diff --git a/src/confd/src/dhcp-client.c b/src/confd/src/dhcp-client.c index 2ef8d1cc4..652b2323b 100644 --- a/src/confd/src/dhcp-client.c +++ b/src/confd/src/dhcp-client.c @@ -180,6 +180,20 @@ int dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config, struc ifaces = lydx_get_descendant(config, "interfaces", "interface", NULL); difaces = lydx_get_descendant(diff, "interfaces", "interface", NULL); + if (diff && lydx_get_xpathf(diff, "/ietf-system:system/hostname")) { + LYX_LIST_FOR_EACH(ifaces, iface, "interface") { + const char *ifname = lydx_get_cattr(iface, "name"); + + ipv4 = lydx_get_descendant(lyd_child(iface), "ipv4", NULL); + if (!ipv4) + continue; + + dhcp = lydx_get_descendant(lyd_child(ipv4), "dhcp", NULL); + if (dhcp) + add(ifname, dhcp); + } + } + /* find the modified interfaces */ LYX_LIST_FOR_EACH(difaces, diface, "interface") { const char *ifname = lydx_get_cattr(diface, "name"); diff --git a/test/case/dhcp/Readme.adoc b/test/case/dhcp/Readme.adoc index a128d72eb..fc29a9d47 100644 --- a/test/case/dhcp/Readme.adoc +++ b/test/case/dhcp/Readme.adoc @@ -7,6 +7,7 @@ Tests verifying DHCPv4/DHCPv6 client and server functionality: - DHCPv4 client with default gateway assignment - DHCPv4 client with static route configuration - DHCPv4 client hostname management and priority + - DHCPv4 client hostname option is resent after system hostname changes - Basic DHCPv6 client operation with address assignment - DHCPv6 client with prefix delegation (IA_PD) - DHCPv6 SLAAC/RA (Stateless) @@ -30,6 +31,10 @@ include::client_hostname/Readme.adoc[] <<< +include::client_hostname_resend/Readme.adoc[] + +<<< + include::client6_basic/Readme.adoc[] <<< diff --git a/test/case/dhcp/client_hostname_resend/Readme.adoc b/test/case/dhcp/client_hostname_resend/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/dhcp/client_hostname_resend/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/dhcp/client_hostname_resend/test.adoc b/test/case/dhcp/client_hostname_resend/test.adoc new file mode 100644 index 000000000..92838425e --- /dev/null +++ b/test/case/dhcp/client_hostname_resend/test.adoc @@ -0,0 +1,29 @@ +=== DHCP Hostname Resend + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/dhcp/client_hostname_resend] + +==== Description + +Verify that updating the system hostname restarts the DHCP client so +subsequent DHCP requests advertise the current hostname (option 12, +RFC 2132). + +Regression test for a bug where the DHCP client callback only reacts +on diffs in infix-dhcp-client, so a standalone change of +ietf-system:system/hostname leaves the running udhcpc untouched with +the old '-x hostname:' argument from when it was first started. + +==== Topology + +image::topology.svg[DHCP Hostname Resend topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure initial hostname '{HOSTNM_A}' +. Enable DHCP client sending hostname option +. Verify running udhcpc announces hostname '{HOSTNM_A}' +. Update system hostname to '{HOSTNM_B}' +. Verify running udhcpc announces hostname '{HOSTNM_B}' + + diff --git a/test/case/dhcp/client_hostname_resend/test.py b/test/case/dhcp/client_hostname_resend/test.py new file mode 100755 index 000000000..b510a1f18 --- /dev/null +++ b/test/case/dhcp/client_hostname_resend/test.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""DHCP Hostname Resend + +Verify that updating the system hostname restarts the DHCP client so +subsequent DHCP requests advertise the current hostname (option 12, +RFC 2132). + +Regression test for a bug where the DHCP client callback only reacts +on diffs in infix-dhcp-client, so a standalone change of +ietf-system:system/hostname leaves the running udhcpc untouched with +the old '-x hostname:' argument from when it was first started. + +""" + +import infamy +from infamy.util import until + + +def udhcpc_cmdline(ssh, ifname): + """Return the NUL-separated argv of the running udhcpc for ifname.""" + pidfile = f"/run/dhcp-client-{ifname}.pid" + cmd = ssh.runsh( + f"p=$(cat {pidfile} 2>/dev/null); " + f"[ -n \"$p\" ] && tr '\\0' ' ' < /proc/$p/cmdline" + ) + return cmd.stdout + + +def running_hostname(ssh, ifname): + """Extract the hostname udhcpc is currently announcing (-x hostname:).""" + for tok in udhcpc_cmdline(ssh, ifname).split(): + if tok.startswith("hostname:"): + return tok.split(":", 1)[1] + return None + + +with infamy.Test() as test: + HOSTNM_A = "infix-resend-a" + HOSTNM_B = "infix-resend-b" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + client = env.attach("client", "mgmt") + clissh = env.attach("client", "mgmt", "ssh") + _, port = env.ltop.xlate("client", "mgmt") + + with test.step(f"Configure initial hostname '{HOSTNM_A}'"): + client.put_config_dict("ietf-system", { + "system": { + "hostname": HOSTNM_A + } + }) + until(lambda: client.get_data("/ietf-system:system") + .get("system", {}).get("hostname") == HOSTNM_A) + + with test.step("Enable DHCP client sending 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", "value": "auto"}, + {"id": "netmask"}, + {"id": "router"} + ] + } + } + }] + } + }) + + with test.step(f"Verify running udhcpc announces hostname '{HOSTNM_A}'"): + until(lambda: running_hostname(clissh, port) == HOSTNM_A) + + with test.step(f"Update system hostname to '{HOSTNM_B}'"): + client.put_config_dict("ietf-system", { + "system": { + "hostname": HOSTNM_B + } + }) + until(lambda: client.get_data("/ietf-system:system") + .get("system", {}).get("hostname") == HOSTNM_B) + + with test.step(f"Verify running udhcpc announces hostname '{HOSTNM_B}'"): + try: + until(lambda: running_hostname(clissh, port) == HOSTNM_B, + attempts=15) + except Exception: + cur = running_hostname(clissh, port) + print(f"udhcpc still announcing hostname '{cur}', expected '{HOSTNM_B}'") + test.fail() + + test.succeed() diff --git a/test/case/dhcp/client_hostname_resend/topology.dot b/test/case/dhcp/client_hostname_resend/topology.dot new file mode 100644 index 000000000..d2b1b2bcb --- /dev/null +++ b/test/case/dhcp/client_hostname_resend/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/dhcp/client_hostname_resend/topology.svg b/test/case/dhcp/client_hostname_resend/topology.svg new file mode 100644 index 000000000..20a632e9f --- /dev/null +++ b/test/case/dhcp/client_hostname_resend/topology.svg @@ -0,0 +1,33 @@ + + + + + + +1x1 + + + +host + +host + +mgmt + + + +client + +mgmt + +client + + + +host:mgmt--client:mgmt + + + + diff --git a/test/case/dhcp/dhcp_client.yaml b/test/case/dhcp/dhcp_client.yaml index 73254d787..ff4827516 100644 --- a/test/case/dhcp/dhcp_client.yaml +++ b/test/case/dhcp/dhcp_client.yaml @@ -14,6 +14,9 @@ - name: DHCP Hostname Priority case: client_hostname/test.py +- name: DHCP Hostname Resend + case: client_hostname_resend/test.py + - name: DHCPv6 Basic case: client6_basic/test.py