Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at netboot-syslinux-multiplatform 222 lines 7.4 kB view raw
1diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py 2index a9a1c980..2d83089b 100644 3--- a/cloudinit/net/dhcp.py 4+++ b/cloudinit/net/dhcp.py 5@@ -14,12 +14,48 @@ from io import StringIO 6 7 import configobj 8 9-from cloudinit import subp, util 10+from cloudinit import subp, util, temp_utils 11 from cloudinit.net import find_fallback_nic, get_devicelist 12 13 LOG = logging.getLogger(__name__) 14 15 NETWORKD_LEASES_DIR = "/run/systemd/netif/leases" 16+UDHCPC_SCRIPT = """#!/bin/sh 17+log() { 18+ echo "udhcpc[$PPID]" "$interface: $2" 19+} 20+ 21+[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1 22+ 23+case $1 in 24+ bound|renew) 25+ cat <<JSON > "$LEASE_FILE" 26+{ 27+ "interface": "$interface", 28+ "fixed-address": "$ip", 29+ "subnet-mask": "$subnet", 30+ "routers": "${router%% *}", 31+ "static_routes" : "${staticroutes}" 32+} 33+JSON 34+ ;; 35+ 36+ deconfig) 37+ log err "Not supported" 38+ exit 1 39+ ;; 40+ 41+ leasefail | nak) 42+ log err "configuration failed: $1: $message" 43+ exit 1 44+ ;; 45+ 46+ *) 47+ echo "$0: Unknown udhcpc command: $1" >&2 48+ exit 1 49+ ;; 50+esac 51+""" 52 53 54 class NoDHCPLeaseError(Exception): 55@@ -43,12 +79,14 @@ class NoDHCPLeaseMissingDhclientError(NoDHCPLeaseError): 56 57 58 def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None, tmp_dir=None): 59- """Perform dhcp discovery if nic valid and dhclient command exists. 60+ """Perform dhcp discovery if nic valid and dhclient or udhcpc command 61+ exists. 62 63 If the nic is invalid or undiscoverable or dhclient command is not found, 64 skip dhcp_discovery and return an empty dict. 65 66- @param nic: Name of the network interface we want to run dhclient on. 67+ @param nic: Name of the network interface we want to run the dhcp client 68+ on. 69 @param dhcp_log_func: A callable accepting the dhclient output and error 70 streams. 71 @param tmp_dir: Tmp dir with exec permissions. 72@@ -66,11 +104,16 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None, tmp_dir=None): 73 "Skip dhcp_discovery: nic %s not found in get_devicelist.", nic 74 ) 75 raise NoDHCPLeaseInterfaceError() 76+ udhcpc_path = subp.which("udhcpc") 77+ if udhcpc_path: 78+ return dhcp_udhcpc_discovery(udhcpc_path, nic, dhcp_log_func) 79 dhclient_path = subp.which("dhclient") 80- if not dhclient_path: 81- LOG.debug("Skip dhclient configuration: No dhclient command found.") 82- raise NoDHCPLeaseMissingDhclientError() 83- return dhcp_discovery(dhclient_path, nic, dhcp_log_func) 84+ if dhclient_path: 85+ return dhcp_discovery(dhclient_path, nic, dhcp_log_func) 86+ LOG.debug( 87+ "Skip dhclient configuration: No dhclient or udhcpc command found." 88+ ) 89+ raise NoDHCPLeaseMissingDhclientError() 90 91 92 def parse_dhcp_lease_file(lease_file): 93@@ -107,6 +150,61 @@ def parse_dhcp_lease_file(lease_file): 94 return dhcp_leases 95 96 97+def dhcp_udhcpc_discovery(udhcpc_cmd_path, interface, dhcp_log_func=None): 98+ """Run udhcpc on the interface without scripts or filesystem artifacts. 99+ 100+ @param udhcpc_cmd_path: Full path to the udhcpc used. 101+ @param interface: Name of the network interface on which to dhclient. 102+ @param dhcp_log_func: A callable accepting the dhclient output and error 103+ streams. 104+ 105+ @return: A list of dicts of representing the dhcp leases parsed from the 106+ dhclient.lease file or empty list. 107+ """ 108+ LOG.debug("Performing a dhcp discovery on %s", interface) 109+ 110+ tmp_dir = temp_utils.get_tmp_ancestor(needs_exe=True) 111+ lease_file = os.path.join(tmp_dir, interface + ".lease.json") 112+ with contextlib.suppress(FileNotFoundError): 113+ os.remove(lease_file) 114+ 115+ # udhcpc needs the interface up to send initial discovery packets. 116+ # Generally dhclient relies on dhclient-script PREINIT action to bring the 117+ # link up before attempting discovery. Since we are using -sf /bin/true, 118+ # we need to do that "link up" ourselves first. 119+ subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True) 120+ udhcpc_script = os.path.join(tmp_dir, "udhcpc_script") 121+ util.write_file(udhcpc_script, UDHCPC_SCRIPT, 0o755) 122+ cmd = [ 123+ udhcpc_cmd_path, 124+ "-O", 125+ "staticroutes", 126+ "-i", 127+ interface, 128+ "-s", 129+ udhcpc_script, 130+ "-n", # Exit if lease is not obtained 131+ "-q", # Exit after obtaining lease 132+ "-f", # Run in foreground 133+ "-v", 134+ ] 135+ 136+ out, err = subp.subp( 137+ cmd, update_env={"LEASE_FILE": lease_file}, capture=True 138+ ) 139+ 140+ if dhcp_log_func is not None: 141+ dhcp_log_func(out, err) 142+ lease_json = util.load_json(util.load_file(lease_file)) 143+ static_routes = lease_json["static_routes"].split() 144+ if static_routes: 145+ # format: dest1/mask gw1 ... destn/mask gwn 146+ lease_json["static_routes"] = [ 147+ i for i in zip(static_routes[::2], static_routes[1::2]) 148+ ] 149+ return [lease_json] 150+ 151+ 152 def dhcp_discovery(dhclient_cmd_path, interface, dhcp_log_func=None): 153 """Run dhclient on the interface without scripts or filesystem artifacts. 154 155diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py 156index 40340553..8913cf65 100644 157--- a/tests/unittests/net/test_dhcp.py 158+++ b/tests/unittests/net/test_dhcp.py 159@@ -12,6 +12,7 @@ from cloudinit.net.dhcp import ( 160 NoDHCPLeaseError, 161 NoDHCPLeaseInterfaceError, 162 NoDHCPLeaseMissingDhclientError, 163+ dhcp_udhcpc_discovery, 164 dhcp_discovery, 165 maybe_perform_dhcp_discovery, 166 networkd_load_leases, 167@@ -334,6 +335,43 @@ class TestDHCPParseStaticRoutes(CiTestCase): 168 ) 169 170 171+class TestUDHCPCDiscoveryClean(CiTestCase): 172+ maxDiff = None 173+ 174+ @mock.patch("cloudinit.net.dhcp.os.remove") 175+ @mock.patch("cloudinit.net.dhcp.subp.subp") 176+ @mock.patch("cloudinit.util.load_json") 177+ @mock.patch("cloudinit.util.load_file") 178+ @mock.patch("cloudinit.util.write_file") 179+ def test_udhcpc_discovery( 180+ self, m_write_file, m_load_file, m_loadjson, m_subp, m_remove 181+ ): 182+ """dhcp_discovery waits for the presence of pidfile and dhcp.leases.""" 183+ m_subp.return_value = ("", "") 184+ m_loadjson.return_value = { 185+ "interface": "eth9", 186+ "fixed-address": "192.168.2.74", 187+ "subnet-mask": "255.255.255.0", 188+ "routers": "192.168.2.1", 189+ "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1", 190+ } 191+ self.assertEqual( 192+ [ 193+ { 194+ "fixed-address": "192.168.2.74", 195+ "interface": "eth9", 196+ "routers": "192.168.2.1", 197+ "static_routes": [ 198+ ("10.240.0.1/32", "0.0.0.0"), 199+ ("0.0.0.0/0", "10.240.0.1"), 200+ ], 201+ "subnet-mask": "255.255.255.0", 202+ } 203+ ], 204+ dhcp_udhcpc_discovery("/sbin/udhcpc", "eth9"), 205+ ) 206+ 207+ 208 class TestDHCPDiscoveryClean(CiTestCase): 209 with_logs = True 210 211@@ -372,7 +410,7 @@ class TestDHCPDiscoveryClean(CiTestCase): 212 maybe_perform_dhcp_discovery() 213 214 self.assertIn( 215- "Skip dhclient configuration: No dhclient command found.", 216+ "Skip dhclient configuration: No dhclient or udhcpc command found.", 217 self.logs.getvalue(), 218 ) 219 220-- 2212.38.4 222