about summary refs log tree commit diff
path: root/pkgs/tools
diff options
context:
space:
mode:
authorWeijia Wang2023-06-20 02:09:40 +0300
committerGitHub2023-06-20 02:09:40 +0300
commit202d01291fe24904540fbc4fb6e75b918ceee2d9 (patch)
tree19c099fc74010b028bd956ce8701a573b657e182 /pkgs/tools
parente1eb7c72c14eb4a93a893916076ec903e787267e (diff)
parent67f5018fe6f71a704c1606da8d2eb2af917972e3 (diff)
Merge pull request #238616 from jfroche/cloud-init-bump
cloud-init: 23.1.2 -> 23.2
Diffstat (limited to 'pkgs/tools')
-rw-r--r--pkgs/tools/virtualization/cloud-init/0001-add-nixos-support.patch16
-rw-r--r--pkgs/tools/virtualization/cloud-init/0002-Add-Udhcpc-support.patch453
-rw-r--r--pkgs/tools/virtualization/cloud-init/0003-vultr-remove-check_route-check.patch110
-rw-r--r--pkgs/tools/virtualization/cloud-init/default.nix8
4 files changed, 340 insertions, 247 deletions
diff --git a/pkgs/tools/virtualization/cloud-init/0001-add-nixos-support.patch b/pkgs/tools/virtualization/cloud-init/0001-add-nixos-support.patch
index f79e3dda8497..f26690bacb70 100644
--- a/pkgs/tools/virtualization/cloud-init/0001-add-nixos-support.patch
+++ b/pkgs/tools/virtualization/cloud-init/0001-add-nixos-support.patch
@@ -12,10 +12,10 @@ index b82852e1..c998b21e 100644
  LOG = logging.getLogger(__name__)
 diff --git a/cloudinit/distros/nixos.py b/cloudinit/distros/nixos.py
 new file mode 100644
-index 00000000..d53d2a61
+index 00000000..d53d2a62
 --- /dev/null
 +++ b/cloudinit/distros/nixos.py
-@@ -0,0 +1,103 @@
+@@ -0,0 +1,109 @@
 +# vi: ts=4 expandtab
 +#
 +#    Copyright (C) 2012 Canonical Ltd.
@@ -47,6 +47,7 @@ index 00000000..d53d2a61
 +from cloudinit import atomic_helper
 +
 +from cloudinit.distros.parsers.hostname import HostnameConf
++from cloudinit.net import dhcp
 +
 +LOG = logging.getLogger(__name__)
 +
@@ -61,6 +62,11 @@ index 00000000..d53d2a61
 +        self.usr_lib_exec = os.path.join(os.path.dirname(__file__),
 +                                         "../../../../../libexec")
 +        self.osfamily = 'nixos'
++        self.dhcp_client_priority = [
++            dhcp.Udhcpc,
++            dhcp.IscDhclient,
++            dhcp.Dhcpcd,
++        ]
 +
 +    def _select_hostname(self, hostname, fqdn):
 +        # Prefer the short hostname over the long
@@ -112,10 +118,10 @@ index 00000000..d53d2a61
 +        raise NotImplementedError()
 +
 +    def package_command(self, command, args=None, pkgs=None):
-+        raise NotImplementedError()
++        pass
 +
 +    def set_timezone(self, tz):
-+        raise NotImplementedError()
++        pass
 +
 +    def update_package_sources(self):
-+        raise NotImplementedError()
++        pass
diff --git a/pkgs/tools/virtualization/cloud-init/0002-Add-Udhcpc-support.patch b/pkgs/tools/virtualization/cloud-init/0002-Add-Udhcpc-support.patch
index ef1694837691..0df3f27a2c40 100644
--- a/pkgs/tools/virtualization/cloud-init/0002-Add-Udhcpc-support.patch
+++ b/pkgs/tools/virtualization/cloud-init/0002-Add-Udhcpc-support.patch
@@ -1,15 +1,56 @@
-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
+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"
  
- import configobj
+     osfamily: str
+-    dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd]
++    dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd, dhcp.Udhcpc]
  
--from cloudinit import subp, util
-+from cloudinit import subp, util, temp_utils
- from cloudinit.net import find_fallback_nic, get_devicelist
+     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"
@@ -17,9 +58,7 @@ index a9a1c980..2d83089b 100644
 +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"
@@ -32,17 +71,14 @@ index a9a1c980..2d83089b 100644
 +}
 +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
@@ -52,134 +88,199 @@ index a9a1c980..2d83089b 100644
  
  
  class NoDHCPLeaseError(Exception):
-@@ -43,12 +79,14 @@ class NoDHCPLeaseMissingDhclientError(NoDHCPLeaseError):
+@@ -50,6 +82,10 @@ class NoDHCPLeaseMissingDhclientError(NoDHCPLeaseError):
+     """Raised when unable to find dhclient."""
  
  
- 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()
- 
++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
  
- def parse_dhcp_lease_file(lease_file):
-@@ -107,6 +150,61 @@ def parse_dhcp_lease_file(lease_file):
-     return dhcp_leases
+@@ -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 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",
+     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,
-+        "-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])
++        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",
 +        ]
-+    return [lease_json]
 +
++        # 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
 +
- def dhcp_discovery(dhclient_cmd_path, interface, dhcp_log_func=None):
-     """Run dhclient on the interface without scripts or filesystem artifacts.
- 
++        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 40340553..8913cf65 100644
+index 55d4c6e9..9123cd15 100644
 --- a/tests/unittests/net/test_dhcp.py
 +++ b/tests/unittests/net/test_dhcp.py
-@@ -12,6 +12,7 @@ from cloudinit.net.dhcp import (
+@@ -13,6 +13,8 @@ from cloudinit.net.dhcp import (
      NoDHCPLeaseError,
      NoDHCPLeaseInterfaceError,
      NoDHCPLeaseMissingDhclientError,
-+    dhcp_udhcpc_discovery,
-     dhcp_discovery,
++    NoDHCPLeaseMissingUdhcpcError,
++    Udhcpc,
      maybe_perform_dhcp_discovery,
      networkd_load_leases,
-@@ -334,6 +335,43 @@ class TestDHCPParseStaticRoutes(CiTestCase):
+ )
+@@ -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
++        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 waits for the presence of pidfile and dhcp.leases."""
++        """dhcp_discovery runs udcpc and parse the dhcp leases."""
 +        m_subp.return_value = ("", "")
 +        m_loadjson.return_value = {
 +            "interface": "eth9",
@@ -201,22 +302,120 @@ index 40340553..8913cf65 100644
 +                    "subnet-mask": "255.255.255.0",
 +                }
 +            ],
-+            dhcp_udhcpc_discovery("/sbin/udhcpc", "eth9"),
++            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,
++                ),
++            ]
 +        )
 +
-+
- 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(),
-         )
- 
++    @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.38.4
+2.40.1
 
diff --git a/pkgs/tools/virtualization/cloud-init/0003-vultr-remove-check_route-check.patch b/pkgs/tools/virtualization/cloud-init/0003-vultr-remove-check_route-check.patch
deleted file mode 100644
index d0d635b939f2..000000000000
--- a/pkgs/tools/virtualization/cloud-init/0003-vultr-remove-check_route-check.patch
+++ /dev/null
@@ -1,110 +0,0 @@
-From 6df2a198013ebed9aeff119ee0d15cb2d616474c Mon Sep 17 00:00:00 2001
-From: zimbatm <zimbatm@zimbatm.com>
-Date: Sun, 30 Apr 2023 12:13:54 +0200
-Subject: [PATCH] vultr: remove check_route check
-
-The heuristic is assuming that the URL will contain an IP, and that the
-route explicitly lists that IP (eg: 0.0.0.0/0 should match but doesn't).
-In order for the heuristic to be 100% reliable, it would have to
-replicate exactly what the system is doing both in terms of DNS and
-route resolution.
-
-Because the HTTP request below is already exercising the python nd
-system resolution, it is simpler to just remove this check and lean on
-the HTTP request to provide the answer if the network is up or not.
----
- cloudinit/sources/helpers/vultr.py    | 22 ----------------------
- tests/unittests/sources/test_vultr.py | 12 ------------
- 2 files changed, 34 deletions(-)
-
-diff --git a/cloudinit/sources/helpers/vultr.py b/cloudinit/sources/helpers/vultr.py
-index 71676bb1..aac2a610 100644
---- a/cloudinit/sources/helpers/vultr.py
-+++ b/cloudinit/sources/helpers/vultr.py
-@@ -32,10 +32,6 @@ def get_metadata(
-                 iface=iface,
-                 connectivity_url_data={"url": url},
-             ):
--                # Check for the metadata route, skip if not there
--                if not check_route(url):
--                    continue
--
-                 # Fetch the metadata
-                 v1 = read_metadata(url, timeout, retries, sec_between, agent)
- 
-@@ -75,24 +71,6 @@ def get_interface_list():
-     return ifaces
- 
- 
--# Check for /32 route that our dhcp servers inject
--# in order to determine if this a customer-run dhcp server
--def check_route(url):
--    # Get routes, confirm entry exists
--    routes = netinfo.route_info()
--
--    # If no tools exist and empty dict is returned
--    if "ipv4" not in routes:
--        return False
--
--    # Parse each route into a more searchable format
--    for route in routes["ipv4"]:
--        if route.get("destination", None) in url:
--            return True
--
--    return False
--
--
- # Read the system information from SMBIOS
- def get_sysinfo():
-     return {
-diff --git a/tests/unittests/sources/test_vultr.py b/tests/unittests/sources/test_vultr.py
-index ba21ae24..7fa02b1c 100644
---- a/tests/unittests/sources/test_vultr.py
-+++ b/tests/unittests/sources/test_vultr.py
-@@ -274,14 +274,6 @@ INTERFACE_MAP = {
- FINAL_INTERFACE_USED = ""
- 
- 
--# Static override, pylint doesnt like this in
--# classes without self
--def check_route(url):
--    if FINAL_INTERFACE_USED == "eth0":
--        return True
--    return False
--
--
- class TestDataSourceVultr(CiTestCase):
-     def setUp(self):
-         global VULTR_V1_3
-@@ -431,7 +423,6 @@ class TestDataSourceVultr(CiTestCase):
-     @mock.patch(
-         "cloudinit.net.ephemeral.EphemeralDHCPv4.__exit__", override_exit
-     )
--    @mock.patch("cloudinit.sources.helpers.vultr.check_route")
-     @mock.patch("cloudinit.sources.helpers.vultr.is_vultr")
-     @mock.patch("cloudinit.sources.helpers.vultr.read_metadata")
-     @mock.patch("cloudinit.sources.helpers.vultr.get_interface_list")
-@@ -440,12 +431,10 @@ class TestDataSourceVultr(CiTestCase):
-         mock_interface_list,
-         mock_read_metadata,
-         mock_isvultr,
--        mock_check_route,
-     ):
-         mock_read_metadata.return_value = {}
-         mock_isvultr.return_value = True
-         mock_interface_list.return_value = FILTERED_INTERFACES
--        mock_check_route.return_value = True
- 
-         distro = mock.MagicMock()
-         distro.get_tmp_exec_path = self.tmp_dir
-@@ -461,7 +450,6 @@ class TestDataSourceVultr(CiTestCase):
-         self.assertEqual(FINAL_INTERFACE_USED, INTERFACES[3])
- 
-     # Test route checking sucessful DHCPs
--    @mock.patch("cloudinit.sources.helpers.vultr.check_route", check_route)
-     @mock.patch(
-         "cloudinit.net.ephemeral.EphemeralDHCPv4.__init__",
-         ephemeral_init_always,
--- 
-2.40.0
-
diff --git a/pkgs/tools/virtualization/cloud-init/default.nix b/pkgs/tools/virtualization/cloud-init/default.nix
index fa9b21defc75..bb3197d53537 100644
--- a/pkgs/tools/virtualization/cloud-init/default.nix
+++ b/pkgs/tools/virtualization/cloud-init/default.nix
@@ -16,22 +16,20 @@
 
 python3.pkgs.buildPythonApplication rec {
   pname = "cloud-init";
-  version = "23.1.2";
+  version = "23.2";
   namePrefix = "";
 
   src = fetchFromGitHub {
     owner = "canonical";
     repo = "cloud-init";
     rev = "refs/tags/${version}";
-    hash = "sha256-tn4flcrf04hVWhqkmK4qDenXcnV93pP+C+8J63b6FXQ=";
+    hash = "sha256-/bhezXS5GunlgID7e/QaTC4UsQ2hDvS5HnG8Wphk64k=";
   };
 
   patches = [
     ./0001-add-nixos-support.patch
-    # upstream: https://github.com/canonical/cloud-init/pull/2125
+    # upstream: https://github.com/canonical/cloud-init/pull/4190
     ./0002-Add-Udhcpc-support.patch
-    # upstream: https://github.com/canonical/cloud-init/pull/2151
-    ./0003-vultr-remove-check_route-check.patch
   ];
 
   prePatch = ''