Ping, still applies.
On Fri Dec 12, 2025 at 12:30 PM CET, Christoph Heiss wrote:
> This adds support for dhcpcd(8) as a second dhcp client. dhclient has
> been deprecated by upstream.
>
> With this patch, ifupdown2 will prefer dhcpcd, falling back to dhclient
> if it cannot find the former for backwards compatibility.
>
> Upstream-PR: https://github.com/CumulusNetworks/ifupdown2/pull/347
> Signed-off-by: Christoph Heiss <[email protected]>
> ---
> To test, first `dhcpcd` must be installed using `apt install dhcpcd`.
>
> Then, put
>
> auto nic1
> iface nic1 inet dhcp
> iface nic8 inet6 dhcp
>
> in /etc/network/interfaces (renaming `nic1` as needed), and commenting
> in/out the respective `inet` or `inet6` line and running
> `ifreload -avd; ip a s nic1` to check the results.
>
> debian/patches/series | 1 +
> ...-support-for-dhcpcd-8-as-dhcp-client.patch | 1009 +++++++++++++++++
> 2 files changed, 1010 insertions(+)
> create mode 100644
> debian/patches/upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-as-dhcp-client.patch
>
> diff --git a/debian/patches/series b/debian/patches/series
> index 2865533..ea7ec89 100644
> --- a/debian/patches/series
> +++ b/debian/patches/series
> @@ -16,3 +16,4 @@
> upstream/0001-use-raw-strings-for-regex-to-fix-backslash-interpret.patch
> upstream/0002-vxlan-add-support-for-IPv6-vxlan-local-tunnelip.patch
> pve/0014-nlmanager-read-ipv6-devconf-disable_ipv6-attribute-t.patch
>
> pve/0015-revert-addons-bond-warn-if-sub-interface-is-detected-on-bond-slave.patch
> +upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-as-dhcp-client.patch
> diff --git
> a/debian/patches/upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-as-dhcp-client.patch
>
> b/debian/patches/upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-as-dhcp-client.patch
> new file mode 100644
> index 0000000..fc9e9cd
> --- /dev/null
> +++
> b/debian/patches/upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-as-dhcp-client.patch
> @@ -0,0 +1,1009 @@
> +From c96ed60826714873ad06315bdfa9ec6028fd2f08 Mon Sep 17 00:00:00 2001
> +From: Christoph Heiss <[email protected]>
> +Date: Fri, 12 Dec 2025 12:05:51 +0100
> +Subject: [PATCH] addons: dhcp: add support for dhcpcd(8) as dhcp client
> +
> +This adds support for dhcpcd(8) as a second dhcp client. dhclient has
> +been deprecated by upstream.
> +
> +With this patch, ifupdown2 will prefer dhcpcd, falling back to dhclient
> +if it cannot find the former for backwards compatibility.
> +
> +Signed-off-by: Christoph Heiss <[email protected]>
> +---
> + docs/source/addonshelperapiref.rst | 18 +++
> + ifupdown2/addons/address.py | 41 ++++---
> + ifupdown2/addons/dhcp.py | 155 +++++++++++++++---------
> + ifupdown2/addons/vrf.py | 34 +++---
> + ifupdown2/ifupdown/exceptions.py | 4 +
> + ifupdown2/ifupdown/utils.py | 18 ++-
> + ifupdown2/ifupdownaddons/dhclient.py | 91 +++++++-------
> + ifupdown2/ifupdownaddons/dhcp_client.py | 49 ++++++++
> + ifupdown2/ifupdownaddons/dhcpcd.py | 147 ++++++++++++++++++++++
> + ifupdown2/lib/sysfs.py | 7 ++
> + 10 files changed, 425 insertions(+), 139 deletions(-)
> + create mode 100644 ifupdown2/ifupdownaddons/dhcp_client.py
> + create mode 100644 ifupdown2/ifupdownaddons/dhcpcd.py
> +
> +diff --git a/docs/source/addonshelperapiref.rst
> b/docs/source/addonshelperapiref.rst
> +index 6eaa0ef..1dd3396 100644
> +--- a/docs/source/addonshelperapiref.rst
> ++++ b/docs/source/addonshelperapiref.rst
> +@@ -22,3 +22,21 @@ Helper module to interact with dhclient tools.
> + .. automodule:: dhclient
> +
> + .. autoclass:: dhclient
> ++
> ++dhcpcd
> ++======
> ++
> ++Helper module to interact with the dhcpcd(8) DHCP client.
> ++
> ++.. automodule:: dhcpcd
> ++
> ++.. autoclass:: dhcpcd
> ++
> ++DhcpClient
> ++==========
> ++
> ++Helper module to interact with the dhcpcd(8) DHCP client.
> ++
> ++.. automodule:: dhcp_client
> ++
> ++.. autoclass:: DhcpClient
> +diff --git a/ifupdown2/addons/address.py b/ifupdown2/addons/address.py
> +index 46226a9..8ae69ee 100644
> +--- a/ifupdown2/addons/address.py
> ++++ b/ifupdown2/addons/address.py
> +@@ -17,8 +17,9 @@ try:
> +
> + from ifupdown2.ifupdown.iface import ifaceType, ifaceLinkKind,
> ifaceLinkPrivFlags, ifaceStatus, iface
> + from ifupdown2.ifupdown.utils import utils
> ++ from ifupdown2.ifupdown.exceptions import NoDhcpClientAvailable
> +
> +- from ifupdown2.ifupdownaddons.dhclient import dhclient
> ++ from ifupdown2.ifupdownaddons.dhcp_client import DhcpClient
> + from ifupdown2.ifupdownaddons.modulebase import moduleBase
> +
> + import ifupdown2.nlmanager.ipnetwork as ipnetwork
> +@@ -33,8 +34,9 @@ except (ImportError, ModuleNotFoundError):
> +
> + from ifupdown.iface import ifaceType, ifaceLinkKind,
> ifaceLinkPrivFlags, ifaceStatus, iface
> + from ifupdown.utils import utils
> ++ from ifupdown.exceptions import NoDhcpClientAvailable
> +
> +- from ifupdownaddons.dhclient import dhclient
> ++ from ifupdownaddons.dhcp_client import DhcpClient
> + from ifupdownaddons.modulebase import moduleBase
> +
> + import nlmanager.ipnetwork as ipnetwork
> +@@ -211,6 +213,8 @@ class address(AddonWithIpBlackList, moduleBase):
> +
> + DEFAULT_MTU_STRING = "1500"
> +
> ++ dhcpcmd: DhcpClient | None
> ++
> + def __init__(self, *args, **kargs):
> + AddonWithIpBlackList.__init__(self)
> + moduleBase.__init__(self, *args, **kargs)
> +@@ -291,6 +295,11 @@ class address(AddonWithIpBlackList, moduleBase):
> + except Exception:
> + self.default_autoconf = 1
> +
> ++ try:
> ++ self.dhcpcmd = DhcpClient()
> ++ except NoDhcpClientAvailable as e:
> ++ self.dhcpcmd = None
> ++
> + def __policy_get_default_mtu(self):
> + default_mtu = policymanager.policymanager_api.get_attr_default(
> + module_name=self.__class__.__name__,
> +@@ -1207,18 +1216,17 @@ class address(AddonWithIpBlackList, moduleBase):
> + if (addr_method not in ["dhcp", "ppp"] and not
> ifupdownflags.flags.PERFMODE and
> + not (ifaceobj.flags & iface.HAS_SIBLINGS)):
> + # if not running in perf mode and ifaceobj does not have
> +- # any sibling iface objects, kill any stale dhclient
> +- # processes
> +- dhclientcmd = dhclient()
> +- if dhclientcmd.is_running(ifaceobj.name):
> +- # release any dhcp leases
> +- dhclientcmd.release(ifaceobj.name)
> +- self.cache.force_address_flush_family(ifaceobj.name,
> socket.AF_INET)
> +- force_reapply = True
> +- elif dhclientcmd.is_running6(ifaceobj.name):
> +- dhclientcmd.release6(ifaceobj.name)
> +- self.cache.force_address_flush_family(ifaceobj.name,
> socket.AF_INET6)
> +- force_reapply = True
> ++ # any sibling iface objects, kill any stale dhcp client
> processes
> ++ if self.dhcpcmd:
> ++ if self.dhcpcmd.is_running(ifaceobj.name):
> ++ # release any dhcp leases
> ++ self.dhcpcmd.release(ifaceobj.name)
> ++
> self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET)
> ++ force_reapply = True
> ++ elif self.dhcpcmd.is_running6(ifaceobj.name):
> ++ self.dhcpcmd.release6(ifaceobj.name)
> ++
> self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET6)
> ++ force_reapply = True
> + except Exception:
> + pass
> +
> +@@ -1624,9 +1632,8 @@ class address(AddonWithIpBlackList, moduleBase):
> +
> + self.query_running_ipv6_addrgen(ifaceobjrunning)
> +
> +- dhclientcmd = dhclient()
> +- if (dhclientcmd.is_running(ifaceobjrunning.name) or
> +- dhclientcmd.is_running6(ifaceobjrunning.name)):
> ++ if self.dhcpcmd and (self.dhcpcmd.is_running(ifaceobjrunning.name)
> or
> ++ self.dhcpcmd.is_running6(ifaceobjrunning.name)):
> + # If dhcp is configured on the interface, we skip it
> + return
> +
> +diff --git a/ifupdown2/addons/dhcp.py b/ifupdown2/addons/dhcp.py
> +index 22bbdb4..cdad890 100644
> +--- a/ifupdown2/addons/dhcp.py
> ++++ b/ifupdown2/addons/dhcp.py
> +@@ -18,8 +18,9 @@ try:
> +
> + from ifupdown2.ifupdown.iface import ifaceLinkPrivFlags, ifaceStatus
> + from ifupdown2.ifupdown.utils import utils
> ++ from ifupdown2.ifupdown.exceptions import moduleNotSupported,
> NoDhcpClientAvailable
> +
> +- from ifupdown2.ifupdownaddons.dhclient import dhclient
> ++ from ifupdown2.ifupdownaddons.dhcp_client import DhcpClient
> + from ifupdown2.ifupdownaddons.modulebase import moduleBase
> + except (ImportError, ModuleNotFoundError):
> + from lib.addon import Addon
> +@@ -30,8 +31,9 @@ except (ImportError, ModuleNotFoundError):
> +
> + from ifupdown.iface import ifaceLinkPrivFlags, ifaceStatus
> + from ifupdown.utils import utils
> ++ from ifupdown.exceptions import moduleNotSupported,
> NoDhcpClientAvailable
> +
> +- from ifupdownaddons.dhclient import dhclient
> ++ from ifupdownaddons.dhcp_client import DhcpClient
> + from ifupdownaddons.modulebase import moduleBase
> +
> +
> +@@ -40,13 +42,14 @@ class dhcp(Addon, moduleBase):
> +
> + # by default we won't perform any dhcp retry
> + # this can be changed by setting the module global
> +- # policy: dhclient_retry_on_failure
> +- DHCLIENT_RETRY_ON_FAILURE = 0
> ++ # policy: dhcp_retry_on_failure
> ++ DHCP_RETRY_ON_FAILURE = 0
> ++
> ++ dhcpcmd: DhcpClient
> +
> + def __init__(self, *args, **kargs):
> + Addon.__init__(self)
> + moduleBase.__init__(self, *args, **kargs)
> +- self.dhclientcmd = dhclient(**kargs)
> + vrf_id = self._get_vrf_context()
> + if vrf_id and vrf_id == 'mgmt':
> + self.mgmt_vrf_context = True
> +@@ -55,24 +58,41 @@ class dhcp(Addon, moduleBase):
> + self.logger.info('mgmt vrf_context = %s' %self.mgmt_vrf_context)
> +
> + try:
> +- self.dhclient_retry_on_failure = int(
> +- policymanager.policymanager_api.get_module_globals(
> +- module_name=self.__class__.__name__,
> +- attr="dhclient_retry_on_failure"
> ++ try:
> ++ self.dhcp_retry_on_failure = int(
> ++ policymanager.policymanager_api.get_module_globals(
> ++ module_name=self.__class__.__name__,
> ++ attr="dhcp_retry_on_failure"
> ++ )
> ++ )
> ++ except:
> ++ self.dhcp_retry_on_failure = int(
> ++ policymanager.policymanager_api.get_module_globals(
> ++ module_name=self.__class__.__name__,
> ++ attr="dhclient_retry_on_failure"
> ++ )
> + )
> +- )
> + except Exception:
> +- self.dhclient_retry_on_failure = self.DHCLIENT_RETRY_ON_FAILURE
> ++ self.dhcp_retry_on_failure = self.DHCP_RETRY_ON_FAILURE
> +
> +- if self.dhclient_retry_on_failure < 0:
> +- self.dhclient_retry_on_failure = 0
> ++ if self.dhcp_retry_on_failure < 0:
> ++ self.dhcp_retry_on_failure = 0
> +
> +- self.logger.debug("dhclient: dhclient_retry_on_failure set to %s" %
> self.dhclient_retry_on_failure)
> ++ try:
> ++ self.dhcpcmd = DhcpClient()
> ++ except NoDhcpClientAvailable as e:
> ++ self.logger.warn('no dhcp client available')
> ++ raise moduleNotSupported(e.message)
> ++
> ++ self.logger.debug("dhcp: dhcp_retry_on_failure set to %s" %
> self.dhcp_retry_on_failure)
> +
> + def syntax_check(self, ifaceobj, ifaceobj_getfunc):
> + return self.is_dhcp_allowed_on(ifaceobj, syntax_check=True)
> +
> + def is_dhcp_allowed_on(self, ifaceobj, syntax_check):
> ++ if not self.dhcpcmd:
> ++ return False
> ++
> + if ifaceobj.addr_method and 'dhcp' in ifaceobj.addr_method:
> + return utils.is_addr_ip_allowed_on(ifaceobj, syntax_check=True)
> + return True
> +@@ -97,9 +117,9 @@ class dhcp(Addon, moduleBase):
> + pass
> + return ips
> +
> +- def dhclient_start_and_check(self, ifname, family, handler, wait=True,
> **handler_kwargs):
> ++ def dhcp_client_start_and_check(self, ifname, family, handler,
> wait=True, **handler_kwargs):
> + ip_config_before = self.get_current_ip_configured(ifname, family)
> +- retry = self.dhclient_retry_on_failure
> ++ retry = self.dhcp_retry_on_failure
> +
> + while retry >= 0:
> + handler(ifname, wait=wait, **handler_kwargs)
> +@@ -107,47 +127,52 @@ class dhcp(Addon, moduleBase):
> + # In most case, the client won't have the time to find
> anything
> + # with the wait=False param.
> + return
> +- retry = self.dhclient_check(ifname, family, ip_config_before,
> retry, handler_kwargs.get("cmd_prefix"))
> ++ retry = self.dhcp_client_check(ifname, family,
> ip_config_before, retry, handler_kwargs.get("cmd_prefix"))
> +
> +- def dhclient_check(self, ifname, family, ip_config_before, retry,
> dhclient_cmd_prefix):
> ++ def dhcp_client_check(self, ifname, family, ip_config_before, retry,
> dhcp_cmd_prefix: list[str]):
> + diff = self.get_current_ip_configured(ifname,
> family).difference(ip_config_before)
> +
> + if diff:
> + self.logger.info(
> +- "%s: dhclient: new address%s detected: %s"
> ++ "%s: dhcp: new address%s detected: %s"
> + % (ifname, "es" if len(diff) > 1 else "", ", ".join(diff))
> + )
> + return -1
> + else:
> + if retry > 0:
> + self.logger.error(
> +- "%s: dhclient: couldn't detect new ip address,
> retrying %s more times..."
> ++ "%s: dhcp: couldn't detect new ip address, retrying
> %s more times..."
> + % (ifname, retry)
> + )
> +- self.dhclientcmd.stop(ifname)
> ++ self.dhcpcmd.stop(ifname)
> + else:
> +- self.logger.error("%s: dhclient: timeout failed to
> detect new ip addresses" % ifname)
> ++ self.logger.error("%s: dhcp: timeout failed to detect
> new ip addresses" % ifname)
> + return -1
> + retry -= 1
> + return retry
> +
> + def _up(self, ifaceobj):
> ++ real_ifname = self.cache.link_translate_altname(ifaceobj.name)
> ++
> + # if dhclient is already running do not stop and start it
> +- dhclient4_running = self.dhclientcmd.is_running(ifaceobj.name)
> +- dhclient6_running = self.dhclientcmd.is_running6(ifaceobj.name)
> ++ dhcp4_running = self.dhcpcmd.is_running(real_ifname)
> ++ dhcp6_running = self.dhcpcmd.is_running6(real_ifname)
> ++
> ++ self.logger.debug(f'dhcp v4 client running: {dhcp4_running}')
> ++ self.logger.debug(f'dhcp v6 client running: {dhcp6_running}')
> +
> + # today if we have an interface with both inet and inet6, if we
> + # remove the inet or inet6 or both then execute ifreload, we need
> + # to release/kill the appropriate dhclient(4/6) if they are running
> +- self._down_stale_dhcp_config(ifaceobj, 'inet', dhclient4_running)
> +- self._down_stale_dhcp_config(ifaceobj, 'inet6', dhclient6_running)
> ++ self._down_stale_dhcp_config(ifaceobj, 'inet', dhcp4_running)
> ++ self._down_stale_dhcp_config(ifaceobj, 'inet6', dhcp6_running)
> +
> + if ifaceobj.link_privflags & ifaceLinkPrivFlags.KEEP_LINK_DOWN:
> + self.logger.info("%s: skipping dhcp configuration: link-down
> yes" % ifaceobj.name)
> + return
> +
> + try:
> +- dhclient_cmd_prefix = None
> ++ dhcp_cmd_prefix = []
> + dhcp_wait = policymanager.policymanager_api.get_attr_default(
> + module_name=self.__class__.__name__, attr='dhcp-wait')
> + wait = str(dhcp_wait).lower() != "no"
> +@@ -163,38 +188,43 @@ class dhcp(Addon, moduleBase):
> + vrf = ifaceobj.get_attr_value_first('vrf')
> + if (vrf and self.vrf_exec_cmd_prefix and
> + self.cache.link_exists(vrf)):
> +- dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix,
> vrf)
> ++ dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrf]
> + elif self.mgmt_vrf_context:
> +- dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix,
> 'default')
> ++ dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() +
> ['default']
> + self.logger.info('detected mgmt vrf context starting
> dhclient in default vrf context')
> +
> + if 'inet' in ifaceobj.addr_family:
> +- if dhclient4_running:
> +- self.logger.info('dhclient4 already running on %s. '
> +- 'Not restarting.' % ifaceobj.name)
> ++ if dhcp4_running:
> ++ self.logger.info('dhcp4 client already running on %s. '
> ++ 'Not restarting.' % real_ifname)
> + else:
> +- # First release any existing dhclient processes
> ++ # First release any existing dhcp processes
> + try:
> + if not ifupdownflags.flags.PERFMODE:
> +- self.dhclientcmd.stop(ifaceobj.name)
> ++ self.dhcpcmd.stop(real_ifname)
> + except Exception:
> + pass
> +
> +- self.dhclient_start_and_check(
> +- ifaceobj.name,
> ++ self.dhcp_client_start_and_check(
> ++ real_ifname,
> + "inet",
> +- self.dhclientcmd.start,
> ++ self.dhcpcmd.start,
> + wait=wait,
> +- cmd_prefix=dhclient_cmd_prefix
> ++ cmd_prefix=dhcp_cmd_prefix
> + )
> ++ elif dhcp4_running:
> ++ # release and stop the running dhcp client if the ipv4 dhcp
> config vanished
> ++ self.logger.debug('dhcp4 running but config vanished,
> stopping')
> ++ self.dhcpcmd.release(real_ifname)
> ++ self.dhcpcmd.stop(real_ifname)
> +
> + if 'inet6' in ifaceobj.addr_family:
> +- if dhclient6_running:
> +- self.logger.info('dhclient6 already running on %s. '
> ++ if dhcp6_running:
> ++ self.logger.info('dhcp6 client already running on %s. '
> + 'Not restarting.' % ifaceobj.name)
> + else:
> + try:
> +- self.dhclientcmd.stop6(ifaceobj.name,
> duid=dhcp6_duid)
> ++ self.dhcpcmd.stop6(real_ifname, duid=dhcp6_duid)
> + except Exception:
> + pass
> + #add delay before starting IPv6 dhclient to
> +@@ -202,17 +232,21 @@ class dhcp(Addon, moduleBase):
> + if timeout > 1:
> + time.sleep(1)
> + while timeout:
> +- addr_output = utils.exec_command('%s -6 addr show
> %s'
> +- %(utils.ip_cmd,
> ifaceobj.name))
> +- r = re.search('inet6 .* scope link', addr_output)
> +- if r:
> +- self.dhclientcmd.start6(ifaceobj.name,
> +- wait=wait,
> +-
> cmd_prefix=dhclient_cmd_prefix, duid=dhcp6_duid)
> +- return
> ++ if self.cache.link_is_up(real_ifname):
> ++ break
> + timeout -= 1
> + if timeout:
> + time.sleep(1)
> ++
> ++ self.dhcpcmd.start6(real_ifname,
> ++ wait=wait,
> ++ cmd_prefix=dhcp_cmd_prefix,
> duid=dhcp6_duid)
> ++ elif dhcp6_running:
> ++ # release and stop the running dhcp client if the ipv6 dhcp
> config vanished
> ++ self.logger.debug('dhcp6 running but config vanished,
> stopping')
> ++ self.dhcpcmd.release6(real_ifname)
> ++ self.dhcpcmd.stop6(real_ifname)
> ++
> + except Exception as e:
> + self.logger.error("%s: %s" % (ifaceobj.name, str(e)))
> + ifaceobj.set_status(ifaceStatus.ERROR)
> +@@ -229,18 +263,20 @@ class dhcp(Addon, moduleBase):
> + ifaceobj.addr_family = addr_family
> +
> + def _dhcp_down(self, ifaceobj):
> +- dhclient_cmd_prefix = None
> ++ dhcp_cmd_prefix = []
> ++ ifname = self.cache.link_translate_altname(ifaceobj.name)
> ++
> + vrf = ifaceobj.get_attr_value_first('vrf')
> + if (vrf and self.vrf_exec_cmd_prefix and
> + self.cache.link_exists(vrf)):
> +- dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, vrf)
> ++ dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrf]
> + dhcp6_duid =
> policymanager.policymanager_api.get_iface_default(module_name=self.__class__.__name__,
> \
> +
> ifname=ifaceobj.name, attr='dhcp6-duid')
> + if 'inet6' in ifaceobj.addr_family:
> +- self.dhclientcmd.release6(ifaceobj.name, dhclient_cmd_prefix,
> duid=dhcp6_duid)
> ++ self.dhcpcmd.release6(ifname, dhcp_cmd_prefix, duid=dhcp6_duid)
> + self.cache.force_address_flush_family(ifaceobj.name,
> socket.AF_INET6)
> + if 'inet' in ifaceobj.addr_family:
> +- self.dhclientcmd.release(ifaceobj.name, dhclient_cmd_prefix)
> ++ self.dhcpcmd.release(ifname, dhcp_cmd_prefix)
> + self.cache.force_address_flush_family(ifaceobj.name,
> socket.AF_INET)
> +
> + def _down(self, ifaceobj):
> +@@ -250,9 +286,10 @@ class dhcp(Addon, moduleBase):
> + def _query_check(self, ifaceobj, ifaceobjcurr):
> + status = ifaceStatus.SUCCESS
> + dhcp_running = False
> ++ ifname = self.cache.link_translate_altname(ifaceobjcurr.name)
> +
> +- dhcp_v4 = self.dhclientcmd.is_running(ifaceobjcurr.name)
> +- dhcp_v6 = self.dhclientcmd.is_running6(ifaceobjcurr.name)
> ++ dhcp_v4 = self.dhcpcmd.is_running(ifname)
> ++ dhcp_v6 = self.dhcpcmd.is_running6(ifname)
> +
> + if dhcp_v4:
> + dhcp_running = True
> +@@ -271,12 +308,14 @@ class dhcp(Addon, moduleBase):
> + ifaceobjcurr.status = status
> +
> + def _query_running(self, ifaceobjrunning):
> +- if not self.cache.link_exists(ifaceobjrunning.name):
> ++ ifname = self.cache.link_translate_altname(ifaceobjrunning.name)
> ++
> ++ if not self.cache.link_exists(ifname):
> + return
> +- if self.dhclientcmd.is_running(ifaceobjrunning.name):
> ++ if self.dhcpcmd.is_running(ifname):
> + ifaceobjrunning.addr_family.append('inet')
> + ifaceobjrunning.addr_method = 'dhcp'
> +- if self.dhclientcmd.is_running6(ifaceobjrunning.name):
> ++ if self.dhcpcmd.is_running6(ifname):
> + ifaceobjrunning.addr_family.append('inet6')
> + ifaceobjrunning.addr_method = 'dhcp6'
> +
> +diff --git a/ifupdown2/addons/vrf.py b/ifupdown2/addons/vrf.py
> +index 2c92a12..04fec56 100644
> +--- a/ifupdown2/addons/vrf.py
> ++++ b/ifupdown2/addons/vrf.py
> +@@ -19,10 +19,11 @@ try:
> +
> + from ifupdown2.ifupdown.iface import ifaceRole, ifaceLinkKind,
> ifaceLinkPrivFlags, ifaceLinkType
> + from ifupdown2.ifupdown.utils import utils
> ++ from ifupdown2.ifupdown.exceptions import NoDhcpClientAvailable
> +
> + from ifupdown2.nlmanager.nlmanager import Link
> +
> +- from ifupdown2.ifupdownaddons.dhclient import dhclient
> ++ from ifupdown2.ifupdownaddons.dhcp_client import DhcpClient
> + from ifupdown2.ifupdownaddons.utilsbase import *
> + from ifupdown2.ifupdownaddons.modulebase import moduleBase
> + except (ImportError, ModuleNotFoundError):
> +@@ -34,14 +35,14 @@ except (ImportError, ModuleNotFoundError):
> +
> + from ifupdown.iface import ifaceRole, ifaceLinkKind,
> ifaceLinkPrivFlags, ifaceLinkType
> + from ifupdown.utils import utils
> ++ from ifupdown.exceptions import NoDhcpClientAvailable
> +
> + from nlmanager.nlmanager import Link
> +
> +- from ifupdownaddons.dhclient import dhclient
> ++ from ifupdownaddons.dhcp_client import DhcpClient
> + from ifupdownaddons.utilsbase import *
> + from ifupdownaddons.modulebase import moduleBase
> +
> +-
> + class vrfPrivFlags:
> + PROCESSED = 0x1
> +
> +@@ -80,10 +81,11 @@ class vrf(Addon, moduleBase):
> + "0": "unspec"
> + }
> +
> ++ dhcpcmd: DhcpClient | None
> ++
> + def __init__(self, *args, **kargs):
> + Addon.__init__(self)
> + moduleBase.__init__(self, *args, **kargs)
> +- self.dhclientcmd = None
> + self.name = self.__class__.__name__
> + self.vrf_mgmt_devname =
> policymanager.policymanager_api.get_module_globals(
> + module_name=self.__class__.__name__,
> +@@ -128,6 +130,11 @@ class vrf(Addon, moduleBase):
> + self.ip6_rule_cache = []
> + self.logger.warning('vrf: cache v6: %s' % str(e))
> +
> ++ try:
> ++ self.dhcpcmd = DhcpClient()
> ++ except NoDhcpClientAvailable as e:
> ++ self.dhcpcmd = None
> ++
> + #self.logger.debug("vrf: ip rule cache")
> + #self.logger.info(self.ip_rule_cache)
> +
> +@@ -403,7 +410,7 @@ class vrf(Addon, moduleBase):
> + def _up_vrf_slave_without_master(self, ifacename, vrfname, ifaceobj,
> vrf_master_objs, ifaceobj_getfunc=None):
> + """ If we have a vrf slave that has dhcp configured, bring up the
> + vrf master now. This is needed because vrf has special handling
> +- in dhclient hook which requires the vrf master to be present """
> ++ in dhcp hook which requires the vrf master to be present """
> + vrf_master = None
> + if len(ifaceobj.upperifaces) > 1 and ifaceobj_getfunc:
> + for upper_iface in ifaceobj.upperifaces:
> +@@ -460,14 +467,15 @@ class vrf(Addon, moduleBase):
> +
> + def _down_dhcp_slave(self, ifaceobj, vrfname):
> + try:
> +- dhclient_cmd_prefix = None
> ++ dhcp_cmd_prefix = None
> + if (vrfname and self.vrf_exec_cmd_prefix and
> + self.cache.link_exists(vrfname)):
> +- dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix,
> +- vrfname)
> +- self.dhclientcmd.release(ifaceobj.name, dhclient_cmd_prefix)
> ++ dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() +
> [vrfname]
> ++
> ++ ifacename = self.cache.link_translate_altname(ifaceobj.name)
> ++ self.dhcpcmd.release(ifacename, dhcp_cmd_prefix)
> + except Exception:
> +- # ignore any dhclient release errors
> ++ # ignore any dhcp client release errors
> + pass
> +
> + def _handle_existing_connections(self, ifaceobj, vrfname):
> +@@ -1127,10 +1135,6 @@ class vrf(Addon, moduleBase):
> + """ returns list of ops supported by this module """
> + return list(self._run_ops.keys())
> +
> +- def _init_command_handlers(self):
> +- if not self.dhclientcmd:
> +- self.dhclientcmd = dhclient()
> +-
> + def run(self, ifaceobj, operation, query_ifaceobj=None,
> + ifaceobj_getfunc=None, **extra_args):
> + """ run bond configuration on the interface object passed as
> argument
> +@@ -1152,7 +1156,7 @@ class vrf(Addon, moduleBase):
> + op_handler = self._run_ops.get(operation)
> + if not op_handler:
> + return
> +- self._init_command_handlers()
> ++
> + if operation == 'query-checkcurr':
> + op_handler(self, ifaceobj, query_ifaceobj)
> + else:
> +diff --git a/ifupdown2/ifupdown/exceptions.py
> b/ifupdown2/ifupdown/exceptions.py
> +index 0dd16a6..54deacd 100644
> +--- a/ifupdown2/ifupdown/exceptions.py
> ++++ b/ifupdown2/ifupdown/exceptions.py
> +@@ -61,3 +61,7 @@ class moduleNotSupported(Error):
> +
> + class ReservedVlanException(Error):
> + pass
> ++
> ++
> ++class NoDhcpClientAvailable(Exception):
> ++ pass
> +diff --git a/ifupdown2/ifupdown/utils.py b/ifupdown2/ifupdown/utils.py
> +index 9b7ec9b..bc7617b 100644
> +--- a/ifupdown2/ifupdown/utils.py
> ++++ b/ifupdown2/ifupdown/utils.py
> +@@ -107,6 +107,7 @@ class utils():
> + ethtool_cmd = '/sbin/ethtool'
> + systemctl_cmd = '/bin/systemctl'
> + dpkg_cmd = '/usr/bin/dpkg'
> ++ dhcpcd_cmd = '/usr/sbin/dhcpcd'
> +
> + logger.info("utils init command paths")
> + for cmd in ['bridge',
> +@@ -123,7 +124,8 @@ class utils():
> + 'mstpctl',
> + 'ethtool',
> + 'systemctl',
> +- 'dpkg'
> ++ 'dpkg',
> ++ 'dhcpcd',
> + ]:
> + if os.path.exists(vars()[cmd + '_cmd']):
> + continue
> +@@ -575,4 +577,18 @@ class utils():
> + raise
> + return vnid
> +
> ++ @staticmethod
> ++ def pid_exists(pid: int) -> bool:
> ++ """
> ++ Check whether there is a process with the given PID.
> ++ """
> ++ try:
> ++ # > If sig is 0, then no signal is sent, but existence and
> permission checks are still
> ++ # > performed; this can be used to check for the existence of a
> process ID or process
> ++ # > group ID that the caller is permitted to signal.
> ++ os.kill(pid, 0)
> ++ except OSError:
> ++ return False
> ++ return True
> ++
> + fcntl.fcntl(utils.DEVNULL, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
> +diff --git a/ifupdown2/ifupdownaddons/dhclient.py
> b/ifupdown2/ifupdownaddons/dhclient.py
> +index c10db65..bfb0208 100644
> +--- a/ifupdown2/ifupdownaddons/dhclient.py
> ++++ b/ifupdown2/ifupdownaddons/dhclient.py
> +@@ -10,49 +10,40 @@ import errno
> + try:
> + from ifupdown2.ifupdown.utils import utils
> + from ifupdown2.ifupdownaddons.utilsbase import *
> ++ from ifupdown2.lib.sysfs import Sysfs
> + except (ImportError, ModuleNotFoundError):
> + from ifupdown.utils import utils
> + from ifupdownaddons.utilsbase import *
> +-
> ++ from lib.sysfs import Sysfs
> +
> + class dhclient(utilsBase):
> + """ This class contains helper methods to interact with the dhclient
> + utility """
> +
> +- def _pid_exists(self, pidfilename):
> +- if os.path.exists(pidfilename):
> +- try:
> +- return os.readlink(
> +- "/proc/%s/exe" % self.read_file_oneline(pidfilename)
> +- ).endswith("dhclient")
> +- except OSError as e:
> +- try:
> +- if e.errno == errno.EACCES:
> +- return os.path.exists("/proc/%s" %
> self.read_file_oneline(pidfilename))
> +- except Exception:
> +- return False
> +- except Exception:
> +- return False
> +- return False
> ++ MAX_RETRIES = 5
> +
> +- def is_running(self, ifacename):
> +- return self._pid_exists('/run/dhclient.%s.pid' %ifacename)
> ++ def __init__(self, *args, **kwargs):
> ++ super().__init__(*args, **kwargs)
> ++ if not os.path.exists('/sbin/dhclient3') and not
> os.path.exists('/sbin/dhclient'):
> ++ raise RuntimeError(f'missing required executable:
> /sbin/dhclient3 or /sbin/dhclient')
> +
> +- def is_running6(self, ifacename):
> +- return self._pid_exists('/run/dhclient6.%s.pid' %ifacename)
> ++ def is_running(self, ifacename: str) -> bool:
> ++ pid = self.read_file_oneline(f'/run/dhclient.{ifacename}.pid')
> ++ try:
> ++ return utils.pid_exists(int(pid))
> ++ except (TypeError, ValueError):
> ++ return False
> +
> +- def _run_dhclient_cmd(self, cmd, cmd_prefix=None):
> +- if not cmd_prefix:
> +- cmd_aslist = []
> +- else:
> +- cmd_aslist = cmd_prefix.split()
> +- if cmd_aslist:
> +- cmd_aslist.extend(cmd)
> +- else:
> +- cmd_aslist = cmd
> +- utils.exec_commandl(cmd_aslist, stdout=None, stderr=None)
> ++ def is_running6(self, ifacename: str) -> bool:
> ++ pid = self.read_file_oneline(f'/run/dhclient6.{ifacename}.pid')
> ++ try:
> ++ return utils.pid_exists(int(pid))
> ++ except (TypeError, ValueError):
> ++ return False
> ++
> ++ def stop(self, ifacename: str, cmd_prefix: list[str] = []):
> ++ self.logger.debug(f'stopping dhclient on {ifacename}')
> +
> +- def stop(self, ifacename, cmd_prefix=None):
> + if os.path.exists('/sbin/dhclient3'):
> + cmd = ['/sbin/dhclient3', '-x', '-pf',
> + '/run/dhclient.%s.pid' %ifacename, '-lf',
> +@@ -63,18 +54,14 @@ class dhclient(utilsBase):
> + '/run/dhclient.%s.pid' %ifacename,
> + '-lf', '/var/lib/dhcp/dhclient.%s.leases' %ifacename,
> + '%s' %ifacename]
> +- self._run_dhclient_cmd(cmd, cmd_prefix)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None, stderr=None)
> +
> +- def start(self, ifacename, wait=True, cmd_prefix=None):
> ++ def start(self, ifacename: str, wait: bool = True, cmd_prefix:
> list[str] = []):
> ++ self.logger.debug(f'starting dhclient on {ifacename}')
> + retries = 0
> +- out = "0"
> +
> + # wait if interface isn't up yet
> +- while '1' not in out and retries < 5:
> +- path = '/sys/class/net/%s/carrier' %ifacename
> +- out = self.read_file_oneline(path)
> +- if out is None:
> +- break # No sysfs file found for this iface
> ++ while not Sysfs.link_has_carrier(ifacename) and retries <
> self.MAX_RETRIES:
> + retries += 1
> + time.sleep(1)
> +
> +@@ -90,9 +77,11 @@ class dhclient(utilsBase):
> + '%s' %ifacename]
> + if not wait:
> + cmd.append('-nw')
> +- self._run_dhclient_cmd(cmd, cmd_prefix)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None, stderr=None)
> ++
> ++ def release(self, ifacename: str, cmd_prefix: list[str] = []):
> ++ self.logger.debug(f'releasing lease on {ifacename}')
> +
> +- def release(self, ifacename, cmd_prefix=None):
> + if os.path.exists('/sbin/dhclient3'):
> + cmd = ['/sbin/dhclient3', '-r', '-pf',
> + '/run/dhclient.%s.pid' %ifacename, '-lf',
> +@@ -103,9 +92,11 @@ class dhclient(utilsBase):
> + '/run/dhclient.%s.pid' %ifacename,
> + '-lf', '/var/lib/dhcp/dhclient.%s.leases' %ifacename,
> + '%s' %ifacename]
> +- self._run_dhclient_cmd(cmd, cmd_prefix)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None, stderr=None)
> ++
> ++ def start6(self, ifacename: str, wait: bool = True, cmd_prefix:
> list[str] = [], duid: str | None = None):
> ++ self.logger.debug(f'starting v6 dhclient on {ifacename}')
> +
> +- def start6(self, ifacename, wait=True, cmd_prefix=None, duid=None):
> + cmd = ['/sbin/dhclient', '-6', '-pf',
> + '/run/dhclient6.%s.pid' %ifacename, '-lf',
> + '/var/lib/dhcp/dhclient6.%s.leases' % ifacename,
> +@@ -115,9 +106,11 @@ class dhclient(utilsBase):
> + if duid is not None:
> + cmd.append('-D')
> + cmd.append(duid)
> +- self._run_dhclient_cmd(cmd, cmd_prefix)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None, stderr=None)
> ++
> ++ def stop6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str |
> None = None):
> ++ self.logger.debug(f'stopping v6 dhclient on {ifacename}')
> +
> +- def stop6(self, ifacename, cmd_prefix=None, duid=None):
> + cmd = ['/sbin/dhclient', '-6', '-x', '-pf',
> + '/run/dhclient6.%s.pid' % ifacename, '-lf',
> + '/var/lib/dhcp/dhclient6.%s.leases' % ifacename,
> +@@ -125,9 +118,11 @@ class dhclient(utilsBase):
> + if duid is not None:
> + cmd.append('-D')
> + cmd.append(duid)
> +- self._run_dhclient_cmd(cmd, cmd_prefix)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None, stderr=None)
> ++
> ++ def release6(self, ifacename: str, cmd_prefix: list[str] = [], duid:
> str | None = None):
> ++ self.logger.debug(f'releasing v6 lease on {ifacename}')
> +
> +- def release6(self, ifacename, cmd_prefix=None, duid=None):
> + cmd = ['/sbin/dhclient', '-6', '-r', '-pf',
> + '/run/dhclient6.%s.pid' %ifacename,
> + '-lf', '/var/lib/dhcp/dhclient6.%s.leases' % ifacename,
> +@@ -135,4 +130,4 @@ class dhclient(utilsBase):
> + if duid is not None:
> + cmd.append('-D')
> + cmd.append(duid)
> +- self._run_dhclient_cmd(cmd, cmd_prefix)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None, stderr=None)
> +diff --git a/ifupdown2/ifupdownaddons/dhcp_client.py
> b/ifupdown2/ifupdownaddons/dhcp_client.py
> +new file mode 100644
> +index 0000000..934119f
> +--- /dev/null
> ++++ b/ifupdown2/ifupdownaddons/dhcp_client.py
> +@@ -0,0 +1,49 @@
> ++import logging
> ++
> ++try:
> ++ from ifupdown2.ifupdownaddons.dhclient import dhclient
> ++ from ifupdown2.ifupdownaddons.dhcpcd import DhcpcdClient
> ++ from ifupdown2.ifupdown.exceptions import NoDhcpClientAvailable
> ++except (ImportError, ModuleNotFoundError):
> ++ from ifupdownaddons.dhclient import dhclient
> ++ from ifupdownaddons.dhcpcd import DhcpcdClient
> ++ from ifupdown.exceptions import NoDhcpClientAvailable
> ++
> ++
> ++class DhcpClient:
> ++ """
> ++ Automatically selects an available DHCP client (preferring dhcpcd over
> dhclient)
> ++ and forwards all method calls to the respective DHCP client.
> ++ """
> ++
> ++ impl: DhcpcdClient | dhclient
> ++
> ++ _PROXIED_METHODS = [
> ++ 'is_running',
> ++ 'is_running6',
> ++ 'start',
> ++ 'start6',
> ++ 'stop',
> ++ 'stop6',
> ++ 'release',
> ++ 'release6',
> ++ ]
> ++
> ++ def __init__(self, *args, **kwargs):
> ++ self.logger = logging.getLogger('ifupdown.dhcp_client')
> ++
> ++ try:
> ++ self.impl = DhcpcdClient(**kwargs)
> ++ self.logger.info('using dhcpcd client')
> ++ except RuntimeError:
> ++ self.logger.debug('dhcpcd client unavailable, trying deprecated
> dhclient')
> ++ try:
> ++ self.impl = dhclient(**kwargs)
> ++ self.logger.info('using deprecated dhclient client')
> ++ except RuntimeError:
> ++ raise NoDhcpClientAvailable('neither dhcpcd nor dhclient
> executable found')
> ++
> ++ def __getattr__(self, name):
> ++ if name in self._PROXIED_METHODS:
> ++ return getattr(self.impl, name)
> ++ raise AttributeError
> +diff --git a/ifupdown2/ifupdownaddons/dhcpcd.py
> b/ifupdown2/ifupdownaddons/dhcpcd.py
> +new file mode 100644
> +index 0000000..fa4eb87
> +--- /dev/null
> ++++ b/ifupdown2/ifupdownaddons/dhcpcd.py
> +@@ -0,0 +1,147 @@
> ++#!/usr/bin/env python3
> ++
> ++import os
> ++import time
> ++
> ++try:
> ++ from ifupdown2.ifupdown.utils import utils
> ++ from ifupdown2.ifupdownaddons.utilsbase import *
> ++ from ifupdown2.lib.sysfs import Sysfs
> ++except (ImportError, ModuleNotFoundError):
> ++ from ifupdown.utils import utils
> ++ from ifupdownaddons.utilsbase import *
> ++ from lib.sysfs import Sysfs
> ++
> ++class DhcpcdClient(utilsBase):
> ++ """
> ++ This class contains helper methods to interact with the dhcpcd(8) client
> ++ """
> ++ MAX_RETRIES = 5
> ++
> ++ def __init__(self, *args, **kwargs):
> ++ super().__init__(*args, **kwargs)
> ++ if not os.path.exists(utils.dhcpcd_cmd):
> ++ raise RuntimeError(f'missing required executable:
> {utils.dhcpcd_cmd}')
> ++
> ++ def _start(self, cmd_prefix: list[str], ifacename: str, wait: bool,
> ipv6: bool, duid: str | None = None):
> ++ """
> ++ Starts the dhcpcd(8) with the given arguments.
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ :param ifacename: Interface name
> ++ :param wait: Whether to wait until a lease has been acquired before
> dhcpcd becomes a daemon.
> ++ :param ipv6: Whether to request a IPv6 address, otherwise IPv4.
> ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT'
> ++ """
> ++ retries = 0
> ++
> ++ # wait if interface isn't up yet
> ++ while not Sysfs.link_has_carrier(ifacename) and retries <
> self.MAX_RETRIES:
> ++ retries += 1
> ++ time.sleep(1)
> ++
> ++ cmd = [utils.dhcpcd_cmd]
> ++
> ++ if ipv6:
> ++ cmd.append('--ipv6only')
> ++ else:
> ++ cmd.append('--ipv4only')
> ++
> ++ if wait:
> ++ cmd.append('--waitip')
> ++
> ++ if duid:
> ++ cmd.extend(['--duid', duid])
> ++
> ++ cmd.append(ifacename)
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++ def is_running(self, ifacename: str) -> bool:
> ++ """
> ++ Checks whether an IPv4 dhcpcd(8) daemon is running for the given
> interface.
> ++ :param ifacename: Interface name
> ++ """
> ++ pid = self.read_file_oneline(f'/run/dhcpcd/{ifacename}-4.pid')
> ++ try:
> ++ return utils.pid_exists(int(pid))
> ++ except (TypeError, ValueError):
> ++ return False
> ++
> ++ def is_running6(self, ifacename: str) -> bool:
> ++ """
> ++ Checks whether an IPv6 dhcpcd(8) daemon is running for the given
> interface.
> ++ :param ifacename: Interface name
> ++ """
> ++ pid = self.read_file_oneline(f'/run/dhcpcd/{ifacename}-6.pid')
> ++ try:
> ++ return utils.pid_exists(int(pid))
> ++ except (TypeError, ValueError):
> ++ return False
> ++
> ++ def start(self, ifacename: str, wait: bool = True, cmd_prefix:
> list[str] = []):
> ++ """
> ++ Starts the dhcpcd(8) for leasing an IPv4 address.
> ++ :param ifacename: Interface name
> ++ :param wait: Whether to wait until a lease has been acquired before
> dhcpcd becomes a daemon.
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ """
> ++ self.logger.debug(f'starting dhcpcd client on {ifacename}')
> ++ self._start(cmd_prefix, ifacename, wait, ipv6=False)
> ++
> ++ def stop(self, ifacename: str, cmd_prefix: list[str] = []):
> ++ """
> ++ Stops the IPv4 dhcpcd daemon for the given interface.
> ++ Does not release the current lease.
> ++ :param ifacename: Interface name
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ """
> ++ self.logger.debug(f'stopping dhcpcd client on {ifacename}')
> ++
> ++ cmd = [utils.dhcpcd_cmd, '--ipv4only', '--exit', ifacename]
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++ def release(self, ifacename: str, cmd_prefix: list[str] = []):
> ++ """
> ++ Releases the current IPv4 lease for the given interface.
> ++ :param ifacename: Interface name
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ """
> ++ self.logger.debug(f'releasing lease on {ifacename}')
> ++
> ++ cmd = [utils.dhcpcd_cmd, '--ipv4only', '--release', ifacename]
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++ def start6(self, ifacename: str, wait: bool = True, cmd_prefix:
> list[str] = [], duid: str | None = None):
> ++ """
> ++ Starts the dhcpcd(8) for leasing an IPv6 address.
> ++ :param ifacename: Interface name
> ++ :param wait: Whether to wait until a lease has been acquired before
> dhcpcd becomes a daemon.
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT'
> ++ """
> ++ self.logger.debug(f'starting v6 dhcpcd client on {ifacename}')
> ++ self._start(cmd_prefix, ifacename, wait, True, duid)
> ++
> ++ def stop6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str |
> None = None):
> ++ """
> ++ Stops the IPv6 dhcpcd daemon for the given interface.
> ++ Does not release the current lease.
> ++ :param ifacename: Interface name
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT'
> ++ """
> ++ self.logger.debug(f'stopping v6 dhcpcd client on {ifacename}')
> ++
> ++ cmd = [utils.dhcpcd_cmd, '--ipv6only', '--exit', ifacename]
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++ def release6(self, ifacename: str, cmd_prefix: list[str] = [], duid:
> str | None = None):
> ++ """
> ++ Releases the current IPv4 lease for the given interface.
> ++ :param ifacename: Interface name
> ++ :param cmd_prefix: Optional list of strings to prefix the dhcpcd
> command with
> ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT'
> ++ """
> ++ self.logger.debug(f'releasing v6 lease on {ifacename}')
> ++
> ++ cmd = [utils.dhcpcd_cmd, '--ipv6only', '--release', ifacename]
> ++ utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> +diff --git a/ifupdown2/lib/sysfs.py b/ifupdown2/lib/sysfs.py
> +index 6aa4284..d3b998b 100644
> +--- a/ifupdown2/lib/sysfs.py
> ++++ b/ifupdown2/lib/sysfs.py
> +@@ -100,6 +100,13 @@ class __Sysfs(IO, Requirements):
> + """
> + return "up" == self.read_file_oneline("/sys/class/net/%s/operstate"
> % ifname)
> +
> ++ def link_has_carrier(self, name: str) -> bool:
> ++ """
> ++ Checks whether the given interface has CARRIER set
> ++ """
> ++ out = self.read_file_oneline(f'/sys/class/net/{name}/carrier')
> ++ return out is not None and '1' in out
> ++
> + def get_link_address(self, ifname):
> + """
> + Read MAC hardware address from sysfs
> +--
> +2.51.2
> +