From 5ff5ea532948c8d3b34e1641ec022a15733459ef Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Fri, 12 Dec 2025 12:04:55 +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. Signed-off-by: Christoph Heiss --- docs/source/addonshelperapiref.rst | 18 +++ ifupdown2/addons/address.py | 40 +++--- ifupdown2/addons/dhcp.py | 164 ++++++++++++++---------- ifupdown2/addons/vrf.py | 32 +++-- ifupdown2/ifupdown/exceptions.py | 4 + ifupdown2/ifupdown/utils.py | 17 ++- ifupdown2/ifupdownaddons/dhclient.py | 99 +++++++------- ifupdown2/ifupdownaddons/dhcp_client.py | 49 +++++++ ifupdown2/ifupdownaddons/dhcpcd.py | 147 +++++++++++++++++++++ ifupdown2/lib/sysfs.py | 7 + 10 files changed, 422 insertions(+), 155 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 6eaa0eff..1dd3396e 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 3d961032..fb10175b 100644 --- a/ifupdown2/addons/address.py +++ b/ifupdown2/addons/address.py @@ -18,8 +18,9 @@ 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 @@ -34,8 +35,9 @@ 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 @@ -200,6 +202,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) @@ -272,6 +276,10 @@ def __init__(self, *args, **kargs): self.mac_regex = re.compile(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") + 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( @@ -1144,18 +1152,17 @@ def _pre_up(self, ifaceobj, ifaceobj_getfunc=None): 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 @@ -1579,9 +1586,8 @@ def _query_running(self, ifaceobjrunning, ifaceobj_getfunc=None): 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 9b2f5f9b..befcf56b 100644 --- a/ifupdown2/addons/dhcp.py +++ b/ifupdown2/addons/dhcp.py @@ -18,8 +18,9 @@ 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: from lib.addon import Addon @@ -30,8 +31,9 @@ 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_DEFAULT_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,19 +58,33 @@ def __init__(self, *args, **kargs): 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_DEFAULT_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 + + try: + self.dhcpcmd = DhcpClient() + except NoDhcpClientAvailable as e: + self.logger.warn('no dhcp client available') + raise moduleNotSupported(e.message) - self.logger.debug("dhclient: dhclient_retry_on_failure set to %s" % self.dhclient_retry_on_failure) + self.logger.debug("dhcp: dhcp_retry_on_failure set to %s" % self.dhcp_retry_on_failure) self.dhclient_no_wait_on_reload = utils.get_boolean_from_string( policymanager.policymanager_api.get_module_globals( @@ -80,6 +97,9 @@ 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 @@ -104,9 +124,9 @@ def get_current_ip_configured(self, ifname, family): 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) @@ -114,48 +134,50 @@ def dhclient_start_and_check(self, ifname, family, handler, wait=True, **handler # 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): # 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(ifaceobj.name) + dhcp6_running = self.dhcpcmd.is_running6(ifaceobj.name) + + 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: bringing dhcp configuration down due to: link-down yes" % ifaceobj.name) - self._dhcp_down(ifaceobj) + 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" @@ -170,9 +192,9 @@ def _up(self, ifaceobj): 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 not ifupdownflags.flags.PERFMODE and self.dhclient_no_wait_on_reload: @@ -180,60 +202,59 @@ def _up(self, ifaceobj): wait = False if 'inet' in ifaceobj.addr_family: - if dhclient4_running: - self.logger.info('dhclient4 already running on %s. ' + if dhcp4_running: + self.logger.info('dhcp4 client already running on %s. ' 'Not restarting.' % ifaceobj.name) 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(ifaceobj.name) except Exception: pass - self.dhclient_start_and_check( + self.dhcp_client_start_and_check( ifaceobj.name, "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(ifaceobj.name) + self.dhcpcmd.stop(ifaceobj.name) 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: - accept_ra = ifaceobj.get_attr_value_first('accept_ra') - if accept_ra: - # XXX: Validate value - self.sysctl_set('net.ipv6.conf.%s' %ifaceobj.name + - '.accept_ra', accept_ra) - autoconf = ifaceobj.get_attr_value_first('autoconf') - if autoconf: - # XXX: Validate value - self.sysctl_set('net.ipv6.conf.%s' %ifaceobj.name + - '.autoconf', autoconf) - try: - self.dhclientcmd.stop6(ifaceobj.name, duid=dhcp6_duid) - except Exception: - pass + try: + self.dhcpcmd.stop6(ifaceobj.name, duid=dhcp6_duid) + except Exception: + pass #add delay before starting IPv6 dhclient to #make sure the configured interface/link is up. 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(ifaceobj.name): + break timeout -= 1 if timeout: time.sleep(1) + + self.dhcpcmd.start6(ifaceobj.name, + 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(ifaceobj.name) + self.dhcpcmd.stop6(ifaceobj.name) + except Exception as e: self.logger.error("%s: %s" % (ifaceobj.name, str(e))) ifaceobj.set_status(ifaceStatus.ERROR) @@ -241,7 +262,7 @@ def _up(self, ifaceobj): def _down_stale_dhcp_config(self, ifaceobj, family, dhclient_running): addr_family = ifaceobj.addr_family try: - if family not in ifaceobj.addr_family and dhclient_running: + if not family in ifaceobj.addr_family and dhclient_running: ifaceobj.addr_family = [family] self._dhcp_down(ifaceobj) except Exception: @@ -250,18 +271,19 @@ def _down_stale_dhcp_config(self, ifaceobj, family, dhclient_running): ifaceobj.addr_family = addr_family def _dhcp_down(self, ifaceobj): - dhclient_cmd_prefix = None + dhcp_cmd_prefix = [] + 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(ifaceobj.name, 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(ifaceobj.name, dhcp_cmd_prefix) self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET) def _down(self, ifaceobj): @@ -272,8 +294,8 @@ def _query_check(self, ifaceobj, ifaceobjcurr): status = ifaceStatus.SUCCESS dhcp_running = False - dhcp_v4 = self.dhclientcmd.is_running(ifaceobjcurr.name) - dhcp_v6 = self.dhclientcmd.is_running6(ifaceobjcurr.name) + dhcp_v4 = self.dhcpcmd.is_running(ifaceobjcurr.name) + dhcp_v6 = self.dhcpcmd.is_running6(ifaceobjcurr.name) if dhcp_v4: dhcp_running = True @@ -294,10 +316,10 @@ def _query_check(self, ifaceobj, ifaceobjcurr): def _query_running(self, ifaceobjrunning): if not self.cache.link_exists(ifaceobjrunning.name): return - if self.dhclientcmd.is_running(ifaceobjrunning.name): + if self.dhcpcmd.is_running(ifaceobjrunning.name): ifaceobjrunning.addr_family.append('inet') ifaceobjrunning.addr_method = 'dhcp' - if self.dhclientcmd.is_running6(ifaceobjrunning.name): + if self.dhcpcmd.is_running6(ifaceobjrunning.name): ifaceobjrunning.addr_family.append('inet6') ifaceobjrunning.addr_method = 'dhcp6' diff --git a/ifupdown2/addons/vrf.py b/ifupdown2/addons/vrf.py index 712c5fd0..205fe936 100644 --- a/ifupdown2/addons/vrf.py +++ b/ifupdown2/addons/vrf.py @@ -19,10 +19,11 @@ 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: @@ -34,10 +35,11 @@ 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 @@ -80,10 +82,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 +131,11 @@ def __init__(self, *args, **kargs): 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.l3mdev_checked = False self.l3mdev4_rule = False if self._l3mdev_rule(self.ip_rule_cache): @@ -394,7 +402,7 @@ def _is_dhcp_slave(self, ifaceobj): 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: @@ -446,14 +454,14 @@ def enable_ipv6_if_prev_brport(self, ifname): 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] + + self.dhcpcmd.release(ifaceobj.name, dhcp_cmd_prefix) except Exception: - # ignore any dhclient release errors + # ignore any dhcp client release errors pass def _handle_existing_connections(self, ifaceobj, vrfname): @@ -1111,10 +1119,6 @@ def get_ops(self): """ 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 @@ -1136,7 +1140,7 @@ def run(self, ifaceobj, operation, query_ifaceobj=None, 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 0dd16a60..54deacd2 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 3a97b725..64e00c19 100644 --- a/ifupdown2/ifupdown/utils.py +++ b/ifupdown2/ifupdown/utils.py @@ -112,6 +112,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', @@ -128,7 +129,8 @@ class utils(): 'mstpctl', 'ethtool', 'systemctl', - 'dpkg' + 'dpkg', + 'dhcpcd', ]: if os.path.exists(vars()[cmd + '_cmd']): continue @@ -679,5 +681,18 @@ def parse_port_list(cls, ifacename, port_expr, ifacenames=None): return None return portlist + @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 f967a8e6..bfb0208b 100644 --- a/ifupdown2/ifupdownaddons/dhclient.py +++ b/ifupdown2/ifupdownaddons/dhclient.py @@ -10,49 +10,40 @@ try: from ifupdown2.ifupdown.utils import utils from ifupdown2.ifupdownaddons.utilsbase import * -except ImportError: + 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 - - def is_running(self, ifacename): - return self._pid_exists('/run/dhclient.%s.pid' %ifacename) - - def is_running6(self, ifacename): - return self._pid_exists('/run/dhclient6.%s.pid' %ifacename) - - 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) + MAX_RETRIES = 5 + + 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_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 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 @@ def stop(self, ifacename, cmd_prefix=None): '/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 @@ def start(self, ifacename, wait=True, cmd_prefix=None): '%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 @@ def release(self, ifacename, cmd_prefix=None): '/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 @@ def start6(self, ifacename, wait=True, cmd_prefix=None, duid=None): 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 @@ def stop6(self, ifacename, cmd_prefix=None, duid=None): 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 @@ def release6(self, ifacename, cmd_prefix=None, duid=None): 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 00000000..934119f2 --- /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 00000000..fa4eb87e --- /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 42577807..5f0c5f55 100644 --- a/ifupdown2/lib/sysfs.py +++ b/ifupdown2/lib/sysfs.py @@ -100,6 +100,13 @@ def link_is_up(self, ifname): """ 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