c7e6337bd0
GitOrigin-RevId: 08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b
222 lines
7.3 KiB
Diff
222 lines
7.3 KiB
Diff
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
|
|
index a9a1c980..2d83089b 100644
|
|
--- a/cloudinit/net/dhcp.py
|
|
+++ b/cloudinit/net/dhcp.py
|
|
@@ -14,12 +14,48 @@ from io import StringIO
|
|
|
|
import configobj
|
|
|
|
-from cloudinit import subp, util
|
|
+from cloudinit import subp, util, temp_utils
|
|
from cloudinit.net import find_fallback_nic, get_devicelist
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
NETWORKD_LEASES_DIR = "/run/systemd/netif/leases"
|
|
+UDHCPC_SCRIPT = """#!/bin/sh
|
|
+log() {
|
|
+ echo "udhcpc[$PPID]" "$interface: $2"
|
|
+}
|
|
+
|
|
+[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1
|
|
+
|
|
+case $1 in
|
|
+ bound|renew)
|
|
+ cat <<JSON > "$LEASE_FILE"
|
|
+{
|
|
+ "interface": "$interface",
|
|
+ "fixed-address": "$ip",
|
|
+ "subnet-mask": "$subnet",
|
|
+ "routers": "${router%% *}",
|
|
+ "static_routes" : "${staticroutes}"
|
|
+}
|
|
+JSON
|
|
+ ;;
|
|
+
|
|
+ deconfig)
|
|
+ log err "Not supported"
|
|
+ exit 1
|
|
+ ;;
|
|
+
|
|
+ leasefail | nak)
|
|
+ log err "configuration failed: $1: $message"
|
|
+ exit 1
|
|
+ ;;
|
|
+
|
|
+ *)
|
|
+ echo "$0: Unknown udhcpc command: $1" >&2
|
|
+ exit 1
|
|
+ ;;
|
|
+esac
|
|
+"""
|
|
|
|
|
|
class NoDHCPLeaseError(Exception):
|
|
@@ -43,12 +79,14 @@ class NoDHCPLeaseMissingDhclientError(NoDHCPLeaseError):
|
|
|
|
|
|
def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None, tmp_dir=None):
|
|
- """Perform dhcp discovery if nic valid and dhclient command exists.
|
|
+ """Perform dhcp discovery if nic valid and dhclient or udhcpc command
|
|
+ exists.
|
|
|
|
If the nic is invalid or undiscoverable or dhclient command is not found,
|
|
skip dhcp_discovery and return an empty dict.
|
|
|
|
- @param nic: Name of the network interface we want to run dhclient on.
|
|
+ @param nic: Name of the network interface we want to run the dhcp client
|
|
+ on.
|
|
@param dhcp_log_func: A callable accepting the dhclient output and error
|
|
streams.
|
|
@param tmp_dir: Tmp dir with exec permissions.
|
|
@@ -66,11 +104,16 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None, tmp_dir=None):
|
|
"Skip dhcp_discovery: nic %s not found in get_devicelist.", nic
|
|
)
|
|
raise NoDHCPLeaseInterfaceError()
|
|
+ udhcpc_path = subp.which("udhcpc")
|
|
+ if udhcpc_path:
|
|
+ return dhcp_udhcpc_discovery(udhcpc_path, nic, dhcp_log_func)
|
|
dhclient_path = subp.which("dhclient")
|
|
- if not dhclient_path:
|
|
- LOG.debug("Skip dhclient configuration: No dhclient command found.")
|
|
- raise NoDHCPLeaseMissingDhclientError()
|
|
- return dhcp_discovery(dhclient_path, nic, dhcp_log_func)
|
|
+ if dhclient_path:
|
|
+ return dhcp_discovery(dhclient_path, nic, dhcp_log_func)
|
|
+ LOG.debug(
|
|
+ "Skip dhclient configuration: No dhclient or udhcpc command found."
|
|
+ )
|
|
+ raise NoDHCPLeaseMissingDhclientError()
|
|
|
|
|
|
def parse_dhcp_lease_file(lease_file):
|
|
@@ -107,6 +150,61 @@ def parse_dhcp_lease_file(lease_file):
|
|
return dhcp_leases
|
|
|
|
|
|
+def dhcp_udhcpc_discovery(udhcpc_cmd_path, interface, dhcp_log_func=None):
|
|
+ """Run udhcpc on the interface without scripts or filesystem artifacts.
|
|
+
|
|
+ @param udhcpc_cmd_path: Full path to the udhcpc used.
|
|
+ @param interface: Name of the network interface on which to dhclient.
|
|
+ @param dhcp_log_func: A callable accepting the dhclient output and error
|
|
+ streams.
|
|
+
|
|
+ @return: A list of dicts of representing the dhcp leases parsed from the
|
|
+ dhclient.lease file or empty list.
|
|
+ """
|
|
+ LOG.debug("Performing a dhcp discovery on %s", interface)
|
|
+
|
|
+ tmp_dir = temp_utils.get_tmp_ancestor(needs_exe=True)
|
|
+ lease_file = os.path.join(tmp_dir, interface + ".lease.json")
|
|
+ with contextlib.suppress(FileNotFoundError):
|
|
+ os.remove(lease_file)
|
|
+
|
|
+ # udhcpc needs the interface up to send initial discovery packets.
|
|
+ # Generally dhclient relies on dhclient-script PREINIT action to bring the
|
|
+ # link up before attempting discovery. Since we are using -sf /bin/true,
|
|
+ # we need to do that "link up" ourselves first.
|
|
+ subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True)
|
|
+ udhcpc_script = os.path.join(tmp_dir, "udhcpc_script")
|
|
+ util.write_file(udhcpc_script, UDHCPC_SCRIPT, 0o755)
|
|
+ cmd = [
|
|
+ udhcpc_cmd_path,
|
|
+ "-O",
|
|
+ "staticroutes",
|
|
+ "-i",
|
|
+ interface,
|
|
+ "-s",
|
|
+ udhcpc_script,
|
|
+ "-n", # Exit if lease is not obtained
|
|
+ "-q", # Exit after obtaining lease
|
|
+ "-f", # Run in foreground
|
|
+ "-v",
|
|
+ ]
|
|
+
|
|
+ out, err = subp.subp(
|
|
+ cmd, update_env={"LEASE_FILE": lease_file}, capture=True
|
|
+ )
|
|
+
|
|
+ if dhcp_log_func is not None:
|
|
+ dhcp_log_func(out, err)
|
|
+ lease_json = util.load_json(util.load_file(lease_file))
|
|
+ static_routes = lease_json["static_routes"].split()
|
|
+ if static_routes:
|
|
+ # format: dest1/mask gw1 ... destn/mask gwn
|
|
+ lease_json["static_routes"] = [
|
|
+ i for i in zip(static_routes[::2], static_routes[1::2])
|
|
+ ]
|
|
+ return [lease_json]
|
|
+
|
|
+
|
|
def dhcp_discovery(dhclient_cmd_path, interface, dhcp_log_func=None):
|
|
"""Run dhclient on the interface without scripts or filesystem artifacts.
|
|
|
|
diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py
|
|
index 40340553..8913cf65 100644
|
|
--- a/tests/unittests/net/test_dhcp.py
|
|
+++ b/tests/unittests/net/test_dhcp.py
|
|
@@ -12,6 +12,7 @@ from cloudinit.net.dhcp import (
|
|
NoDHCPLeaseError,
|
|
NoDHCPLeaseInterfaceError,
|
|
NoDHCPLeaseMissingDhclientError,
|
|
+ dhcp_udhcpc_discovery,
|
|
dhcp_discovery,
|
|
maybe_perform_dhcp_discovery,
|
|
networkd_load_leases,
|
|
@@ -334,6 +335,43 @@ class TestDHCPParseStaticRoutes(CiTestCase):
|
|
)
|
|
|
|
|
|
+class TestUDHCPCDiscoveryClean(CiTestCase):
|
|
+ maxDiff = None
|
|
+
|
|
+ @mock.patch("cloudinit.net.dhcp.os.remove")
|
|
+ @mock.patch("cloudinit.net.dhcp.subp.subp")
|
|
+ @mock.patch("cloudinit.util.load_json")
|
|
+ @mock.patch("cloudinit.util.load_file")
|
|
+ @mock.patch("cloudinit.util.write_file")
|
|
+ def test_udhcpc_discovery(
|
|
+ self, m_write_file, m_load_file, m_loadjson, m_subp, m_remove
|
|
+ ):
|
|
+ """dhcp_discovery waits for the presence of pidfile and dhcp.leases."""
|
|
+ m_subp.return_value = ("", "")
|
|
+ m_loadjson.return_value = {
|
|
+ "interface": "eth9",
|
|
+ "fixed-address": "192.168.2.74",
|
|
+ "subnet-mask": "255.255.255.0",
|
|
+ "routers": "192.168.2.1",
|
|
+ "static_routes": "10.240.0.1/32 0.0.0.0 0.0.0.0/0 10.240.0.1",
|
|
+ }
|
|
+ self.assertEqual(
|
|
+ [
|
|
+ {
|
|
+ "fixed-address": "192.168.2.74",
|
|
+ "interface": "eth9",
|
|
+ "routers": "192.168.2.1",
|
|
+ "static_routes": [
|
|
+ ("10.240.0.1/32", "0.0.0.0"),
|
|
+ ("0.0.0.0/0", "10.240.0.1"),
|
|
+ ],
|
|
+ "subnet-mask": "255.255.255.0",
|
|
+ }
|
|
+ ],
|
|
+ dhcp_udhcpc_discovery("/sbin/udhcpc", "eth9"),
|
|
+ )
|
|
+
|
|
+
|
|
class TestDHCPDiscoveryClean(CiTestCase):
|
|
with_logs = True
|
|
|
|
@@ -372,7 +410,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
|
|
maybe_perform_dhcp_discovery()
|
|
|
|
self.assertIn(
|
|
- "Skip dhclient configuration: No dhclient command found.",
|
|
+ "Skip dhclient configuration: No dhclient or udhcpc command found.",
|
|
self.logs.getvalue(),
|
|
)
|
|
|
|
--
|
|
2.38.4
|
|
|