cloud-init: add udhcpc support (#226216)
* cloud-init: 22.4 -> 23.1.1 * cloud-init: add udhcpc support Cloud-init use as dhcp client, dhclient, which is coming from the unmaintained package, isc-dhcp-client (refer https://www.isc.org/dhcp/) which ended support in 2022. dhclient is deprecated in nixos Add patch to use `udhcpc` dhcp client coming from busybox instead. PR based on #226173 refs #215571 upstream PR: https://github.com/canonical/cloud-init/pull/2125
This commit is contained in:
parent
c1e467b13c
commit
25671114cd
4 changed files with 240 additions and 11 deletions
|
@ -10,6 +10,7 @@ let cfg = config.services.cloud-init;
|
|||
openssh
|
||||
shadow
|
||||
util-linux
|
||||
busybox
|
||||
] ++ optional cfg.btrfs.enable btrfs-progs
|
||||
++ optional cfg.ext4.enable e2fsprogs
|
||||
;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
|
||||
index 4a468cf8..c60c899b 100644
|
||||
index b82852e1..c998b21e 100644
|
||||
--- a/cloudinit/distros/__init__.py
|
||||
+++ b/cloudinit/distros/__init__.py
|
||||
@@ -55,6 +55,7 @@ OSFAMILIES = {
|
||||
"virtuozzo",
|
||||
@@ -74,6 +74,7 @@ OSFAMILIES = {
|
||||
],
|
||||
"suse": ["opensuse", "sles"],
|
||||
"openEuler": ["openEuler"],
|
||||
"OpenCloudOS": ["OpenCloudOS", "TencentOS"],
|
||||
+ "nixos": ["nixos"],
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
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
|
||||
|
|
@ -10,21 +10,23 @@
|
|||
, shadow
|
||||
, systemd
|
||||
, coreutils
|
||||
, gitUpdater
|
||||
, busybox
|
||||
}:
|
||||
|
||||
python3.pkgs.buildPythonApplication rec {
|
||||
pname = "cloud-init";
|
||||
version = "22.4";
|
||||
version = "23.1.1";
|
||||
namePrefix = "";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "canonical";
|
||||
repo = "cloud-init";
|
||||
rev = "refs/tags/${version}";
|
||||
hash = "sha256-MsT5t2da79Eb9FlTLPr2893JcF0ujNnToJTCQRT1QEo=";
|
||||
hash = "sha256-w1UP7JIt/+6UlASB8kv2Lil+1sMTDIrADoYOT/WtaeE=";
|
||||
};
|
||||
|
||||
patches = [ ./0001-add-nixos-support.patch ];
|
||||
patches = [ ./0001-add-nixos-support.patch ./0002-Add-Udhcpc-support.patch ];
|
||||
|
||||
prePatch = ''
|
||||
substituteInPlace setup.py \
|
||||
|
@ -72,7 +74,7 @@ python3.pkgs.buildPythonApplication rec {
|
|||
];
|
||||
|
||||
makeWrapperArgs = [
|
||||
"--prefix PATH : ${lib.makeBinPath [ dmidecode cloud-utils.guest ]}/bin"
|
||||
"--prefix PATH : ${lib.makeBinPath [ dmidecode cloud-utils.guest busybox ]}/bin"
|
||||
];
|
||||
|
||||
disabledTests = [
|
||||
|
@ -82,6 +84,7 @@ python3.pkgs.buildPythonApplication rec {
|
|||
"test_path_env_gets_set_from_main"
|
||||
# tries to read from /etc/ca-certificates.conf while inside the sandbox
|
||||
"test_handler_ca_certs"
|
||||
"TestRemoveDefaultCaCerts"
|
||||
# Doesn't work in the sandbox
|
||||
"TestEphemeralDhcpNoNetworkSetup"
|
||||
"TestHasURLConnectivity"
|
||||
|
@ -112,13 +115,16 @@ python3.pkgs.buildPythonApplication rec {
|
|||
"cloudinit"
|
||||
];
|
||||
|
||||
passthru.tests = { inherit (nixosTests) cloud-init cloud-init-hostname; };
|
||||
passthru = {
|
||||
tests = { inherit (nixosTests) cloud-init cloud-init-hostname; };
|
||||
updateScript = gitUpdater { ignoredVersions = ".ubuntu.*"; };
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://cloudinit.readthedocs.org";
|
||||
homepage = "https://github.com/canonical/cloud-init";
|
||||
description = "Provides configuration and customization of cloud instance";
|
||||
license = with licenses; [ asl20 gpl3Plus ];
|
||||
maintainers = with maintainers; [ illustris ];
|
||||
maintainers = with maintainers; [ illustris jfroche ];
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue