nixpkgs/pkgs/tools/virtualization/cloud-init/0002-Add-Udhcpc-support.patch
Jean-François Roche 67f5018fe6 cloud-init: 23.1.2 -> 23.2
Keep support for udhcpc (waiting for upstream PR:
https://github.com/canonical/cloud-init/pull/4190).

Vultr patch has been merged upstream
(https://github.com/canonical/cloud-init/pull/2151).
2023-06-19 18:09:18 +02:00

422 lines
15 KiB
Diff

From 53260ce3bd70a0852d3e0d5569474214cea0ec0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= <jfroche@pyxel.be>
Date: Mon, 19 Jun 2023 15:56:46 +0200
Subject: [PATCH] net/dhcp: add udhcpc support
The currently used dhcp client, dhclient, is coming from the unmaintained package, isc-dhcp-client (refer https://www.isc.org/dhcp/) which ended support in 2022.
This change introduce support for the dhcp client, udhcpc, from the busybox project. Busybox advantages are that it is available across many distributions and comes with lightweight executables.
---
cloudinit/distros/__init__.py | 8 +-
cloudinit/net/dhcp.py | 129 ++++++++++++++++++++++-
tests/unittests/net/test_dhcp.py | 175 ++++++++++++++++++++++++++++++-
tools/.github-cla-signers | 1 +
4 files changed, 309 insertions(+), 4 deletions(-)
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index ec148939..0fab8945 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -110,14 +110,18 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
resolve_conf_fn = "/etc/resolv.conf"
osfamily: str
- dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd]
+ dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd, dhcp.Udhcpc]
def __init__(self, name, cfg, paths):
self._paths = paths
self._cfg = cfg
self.name = name
self.networking: Networking = self.networking_cls()
- self.dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd]
+ self.dhcp_client_priority = [
+ dhcp.IscDhclient,
+ dhcp.Dhcpcd,
+ dhcp.Udhcpc,
+ ]
def _unpickle(self, ci_pkl_version: int) -> None:
"""Perform deserialization fixes for Distro."""
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 6c8c2f54..f5586cea 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -21,6 +21,7 @@ from cloudinit import subp, temp_utils, util
from cloudinit.net import (
find_fallback_nic,
get_devicelist,
+ get_ib_interface_hwaddr,
get_interface_mac,
is_ib_interface,
)
@@ -28,6 +29,37 @@ from cloudinit.net import (
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):
@@ -50,6 +82,10 @@ class NoDHCPLeaseMissingDhclientError(NoDHCPLeaseError):
"""Raised when unable to find dhclient."""
+class NoDHCPLeaseMissingUdhcpcError(NoDHCPLeaseError):
+ """Raised when unable to find udhcpc client."""
+
+
def select_dhcp_client(distro):
"""distros set priority list, select based on this order which to use
@@ -60,7 +96,10 @@ def select_dhcp_client(distro):
dhcp_client = client()
LOG.debug("DHCP client selected: %s", client.client_name)
return dhcp_client
- except NoDHCPLeaseMissingDhclientError:
+ except (
+ NoDHCPLeaseMissingDhclientError,
+ NoDHCPLeaseMissingUdhcpcError,
+ ):
LOG.warning("DHCP client not found: %s", client.client_name)
raise NoDHCPLeaseMissingDhclientError()
@@ -497,3 +536,91 @@ class Dhcpcd:
def __init__(self):
raise NoDHCPLeaseMissingDhclientError("Dhcpcd not yet implemented")
+
+
+class Udhcpc(DhcpClient):
+ client_name = "udhcpc"
+
+ def __init__(self):
+ self.udhcpc_path = subp.which("udhcpc")
+ if not self.udhcpc_path:
+ LOG.debug("Skip udhcpc configuration: No udhcpc command found.")
+ raise NoDHCPLeaseMissingUdhcpcError()
+
+ def dhcp_discovery(
+ self,
+ interface,
+ dhcp_log_func=None,
+ distro=None,
+ ):
+ """Run udhcpc on the interface without scripts or filesystem artifacts.
+
+ @param interface: Name of the network interface on which to run udhcpc.
+ @param dhcp_log_func: A callable accepting the udhcpc output and
+ error streams.
+
+ @return: A list of dicts of representing the dhcp leases parsed from
+ the udhcpc lease file.
+ """
+ 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
+ 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 = [
+ self.udhcpc_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",
+ ]
+
+ # For INFINIBAND port the dhcpc must be running with
+ # client id option. So here we are checking if the interface is
+ # INFINIBAND or not. If yes, we are generating the the client-id to be
+ # used with the udhcpc
+ if is_ib_interface(interface):
+ dhcp_client_identifier = get_ib_interface_hwaddr(
+ interface, ethernet_format=True
+ )
+ cmd.extend(
+ ["-x", "0x3d:%s" % dhcp_client_identifier.replace(":", "")]
+ )
+ try:
+ out, err = subp.subp(
+ cmd, update_env={"LEASE_FILE": lease_file}, capture=True
+ )
+ except subp.ProcessExecutionError as error:
+ LOG.debug(
+ "udhcpc exited with code: %s stderr: %r stdout: %r",
+ error.exit_code,
+ error.stderr,
+ error.stdout,
+ )
+ raise NoDHCPLeaseError from error
+
+ 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]
diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py
index 55d4c6e9..9123cd15 100644
--- a/tests/unittests/net/test_dhcp.py
+++ b/tests/unittests/net/test_dhcp.py
@@ -13,6 +13,8 @@ from cloudinit.net.dhcp import (
NoDHCPLeaseError,
NoDHCPLeaseInterfaceError,
NoDHCPLeaseMissingDhclientError,
+ NoDHCPLeaseMissingUdhcpcError,
+ Udhcpc,
maybe_perform_dhcp_discovery,
networkd_load_leases,
)
@@ -388,11 +390,13 @@ class TestDHCPDiscoveryClean(CiTestCase):
self.logs.getvalue(),
)
+ @mock.patch("cloudinit.temp_utils.get_tmp_ancestor", return_value="/tmp")
@mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.dhcp.subp.which")
- def test_dhcp_client_failover(self, m_which, m_subp, m_remove, m_fallback):
+ def test_dhcp_client_failover(self, m_which, m_subp, m_remove, m_fallback,
+ m_get_tmp_ancestor):
"""Log and do nothing when nic is absent and no fallback is found."""
m_subp.side_effect = [
("", ""),
@@ -928,3 +932,172 @@ class TestEphemeralDhcpLeaseErrors:
pass
assert len(m_dhcp.mock_calls) == 1
+
+
+class TestUDHCPCDiscoveryClean(CiTestCase):
+ with_logs = True
+ maxDiff = None
+
+ @mock.patch("cloudinit.temp_utils.get_tmp_ancestor", return_value="/tmp")
+ @mock.patch("cloudinit.net.dhcp.subp.which")
+ @mock.patch("cloudinit.net.dhcp.find_fallback_nic")
+ def test_absent_udhcpc_command(self, m_fallback, m_which,
+ m_get_tmp_ancestor):
+ """When dhclient doesn't exist in the OS, log the issue and no-op."""
+ m_fallback.return_value = "eth9"
+ m_which.return_value = None # udhcpc isn't found
+
+ distro = MockDistro()
+ distro.dhcp_client_priority = [Udhcpc]
+
+ with pytest.raises(NoDHCPLeaseMissingDhclientError):
+ maybe_perform_dhcp_discovery(distro)
+
+ self.assertIn(
+ "Skip udhcpc configuration: No udhcpc command found.",
+ self.logs.getvalue(),
+ )
+
+ @mock.patch("cloudinit.temp_utils.get_tmp_ancestor", return_value="/tmp")
+ @mock.patch("cloudinit.net.dhcp.is_ib_interface", return_value=False)
+ @mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/udhcpc")
+ @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,
+ m_which,
+ mocked_is_ib_interface,
+ m_get_tmp_ancestor,
+ ):
+ """dhcp_discovery runs udcpc and parse the 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",
+ }
+ ],
+ Udhcpc().dhcp_discovery("eth9", distro=MockDistro()),
+ )
+ # Interface was brought up before dhclient called
+ m_subp.assert_has_calls(
+ [
+ mock.call(
+ ["ip", "link", "set", "dev", "eth9", "up"],
+ capture=True,
+ ),
+ mock.call(
+ [
+ "/sbin/udhcpc",
+ "-O",
+ "staticroutes",
+ "-i",
+ "eth9",
+ "-s",
+ "/tmp/udhcpc_script",
+ "-n",
+ "-q",
+ "-f",
+ "-v",
+ ],
+ update_env={"LEASE_FILE": "/tmp/eth9.lease.json"},
+ capture=True,
+ ),
+ ]
+ )
+
+ @mock.patch("cloudinit.temp_utils.get_tmp_ancestor", return_value="/tmp")
+ @mock.patch("cloudinit.net.dhcp.is_ib_interface", return_value=True)
+ @mock.patch("cloudinit.net.dhcp.get_ib_interface_hwaddr")
+ @mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/udhcpc")
+ @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_ib(
+ self,
+ m_write_file,
+ m_load_file,
+ m_loadjson,
+ m_subp,
+ m_remove,
+ m_which,
+ m_get_ib_interface_hwaddr,
+ m_is_ib_interface,
+ m_get_tmp_ancestor,
+ ):
+ """dhcp_discovery runs udcpc and parse the dhcp leases."""
+ m_subp.return_value = ("", "")
+ m_loadjson.return_value = {
+ "interface": "ib0",
+ "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",
+ }
+ m_get_ib_interface_hwaddr.return_value = "00:21:28:00:01:cf:4b:01"
+ self.assertEqual(
+ [
+ {
+ "fixed-address": "192.168.2.74",
+ "interface": "ib0",
+ "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",
+ }
+ ],
+ Udhcpc().dhcp_discovery("ib0", distro=MockDistro()),
+ )
+ # Interface was brought up before dhclient called
+ m_subp.assert_has_calls(
+ [
+ mock.call(
+ ["ip", "link", "set", "dev", "ib0", "up"], capture=True
+ ),
+ mock.call(
+ [
+ "/sbin/udhcpc",
+ "-O",
+ "staticroutes",
+ "-i",
+ "ib0",
+ "-s",
+ "/tmp/udhcpc_script",
+ "-n",
+ "-q",
+ "-f",
+ "-v",
+ "-x",
+ "0x3d:0021280001cf4b01",
+ ],
+ update_env={"LEASE_FILE": "/tmp/ib0.lease.json"},
+ capture=True,
+ ),
+ ]
+ )
diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
index b4a9326e..4d82a055 100644
--- a/tools/.github-cla-signers
+++ b/tools/.github-cla-signers
@@ -65,6 +65,7 @@ jacobsalmela
jamesottinger
Jehops
jf
+jfroche
Jille
JohnKepplers
johnsonshi
--
2.40.1