Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions cloudinit/distros/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,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,
]
self.net_ops = iproute2.Iproute2

def _unpickle(self, ci_pkl_version: int) -> None:
Expand Down
129 changes: 128 additions & 1 deletion cloudinit/net/dhcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,45 @@
from cloudinit.net import (
find_fallback_nic,
get_devicelist,
get_ib_interface_hwaddr,
get_interface_mac,
is_ib_interface,
)

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):
Expand All @@ -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

Expand All @@ -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()

Expand Down Expand Up @@ -492,3 +531,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
distro.net_ops.link_up(interface)

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]
167 changes: 167 additions & 0 deletions tests/unittests/net/test_dhcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
NoDHCPLeaseError,
NoDHCPLeaseInterfaceError,
NoDHCPLeaseMissingDhclientError,
Udhcpc,
maybe_perform_dhcp_discovery,
networkd_load_leases,
)
Expand Down Expand Up @@ -928,3 +929,169 @@ def test_ctx_mgr_umbrella_error(self, m_dhcp, error_class):
pass

assert len(m_dhcp.mock_calls) == 1


class TestUDHCPCDiscoveryClean(CiTestCase):
with_logs = True
maxDiff = None

@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):
"""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.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,
):
"""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"],
),
mock.call(
[
"/sbin/udhcpc",
"-O",
"staticroutes",
"-i",
"eth9",
"-s",
"/var/tmp/cloud-init/udhcpc_script",
"-n",
"-q",
"-f",
"-v",
],
update_env={
"LEASE_FILE": "/var/tmp/cloud-init/eth9.lease.json"
},
capture=True,
),
]
)

@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,
):
"""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"],
),
mock.call(
[
"/sbin/udhcpc",
"-O",
"staticroutes",
"-i",
"ib0",
"-s",
"/var/tmp/cloud-init/udhcpc_script",
"-n",
"-q",
"-f",
"-v",
"-x",
"0x3d:0021280001cf4b01",
],
update_env={
"LEASE_FILE": "/var/tmp/cloud-init/ib0.lease.json"
},
capture=True,
),
]
)
1 change: 1 addition & 0 deletions tools/.github-cla-signers
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jacobsalmela
jamesottinger
Jehops
jf
jfroche
Jille
JohnKepplers
johnsonshi
Expand Down