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 + -- 2.51.2 _______________________________________________ pve-devel mailing list [email protected] https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
