Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi dhcp client #253

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
66 changes: 41 additions & 25 deletions ifupdown2/addons/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from ifupdown2.ifupdown.iface import *
from ifupdown2.ifupdown.utils import utils

from ifupdown2.ifupdownaddons.dhclient import dhclient
from ifupdown2.addons.dhcp import dhcp
from ifupdown2.ifupdownaddons.modulebase import moduleBase

import ifupdown2.nlmanager.ipnetwork as ipnetwork
Expand All @@ -34,7 +34,7 @@
from ifupdown.iface import *
from ifupdown.utils import utils

from ifupdownaddons.dhclient import dhclient
from addons.dhcp import dhcp
from ifupdownaddons.modulebase import moduleBase

import nlmanager.ipnetwork as ipnetwork
Expand Down Expand Up @@ -1035,6 +1035,41 @@ def up_ipv6_addrgen(self, ifaceobj):
else:
self.logger.warning('%s: invalid value "%s" for attribute ipv6-addrgen' % (ifaceobj.name, user_configured_ipv6_addrgen))

def _release_stale_dhcp(self, ifaceobj):
""" Release any stale dhcp clients.

This method won't take actions when:
* ifupdown2 is running in perf mode.
* the interface must be handled by dhcp or ppp addons.
* the interface has any sibling.

Returns:
bool: True if a client has been released, False otherwise.
"""
if ifaceobj.addr_method in ["dhcp", "ppp"]:
return False
if ifupdownflags.flags.PERFMODE:
return False
if ifaceobj.flags & iface.HAS_SIBLINGS:
return False

ifname = ifaceobj.name
released = False
for cls in dhcp.DHCP_CLIENTS.values():
try:
client = cls()
if client.is_running(ifname):
client.release(ifname)
self.cache.force_address_flush_family(ifname, socket.AF_INET)
released = True
elif client.is_running6(ifname):
client.release6(ifname)
self.cache.force_address_flush_family(ifname, socket.AF_INET6)
released = True
except Exception:
pass
return released

def _pre_up(self, ifaceobj, ifaceobj_getfunc=None):
if not self.cache.link_exists(ifaceobj.name):
return
Expand All @@ -1050,26 +1085,7 @@ def _pre_up(self, ifaceobj, ifaceobj_getfunc=None):
self._sysctl_config(ifaceobj)

addr_method = ifaceobj.addr_method
force_reapply = False
try:
# release any stale dhcp addresses if present
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
except Exception:
pass
force_reapply = self._release_stale_dhcp(ifaceobj)

self.process_mtu(ifaceobj, ifaceobj_getfunc)
self.up_ipv6_addrgen(ifaceobj)
Expand Down Expand Up @@ -1434,9 +1450,9 @@ 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)):
clients = (cls() for cls in dhcp.DHCP_CLIENTS.values())
if any(client.is_running(ifaceobjrunning.name) or
client.is_running6(ifaceobjrunning.name) for client in clients):
# If dhcp is configured on the interface, we skip it
return

Expand Down
64 changes: 50 additions & 14 deletions ifupdown2/addons/dhcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ifupdown2.ifupdown.utils import utils

from ifupdown2.ifupdownaddons.dhclient import dhclient
from ifupdown2.ifupdownaddons.udhcpc import udhcpc
from ifupdown2.ifupdownaddons.modulebase import moduleBase
except (ImportError, ModuleNotFoundError):
from lib.addon import Addon
Expand All @@ -31,6 +32,7 @@
from ifupdown.utils import utils

from ifupdownaddons.dhclient import dhclient
from ifupdownaddons.udhcpc import udhcpc
from ifupdownaddons.modulebase import moduleBase


Expand All @@ -41,11 +43,44 @@ class dhcp(Addon, moduleBase):
# this can be changed by setting the module global
# policy: dhclient_retry_on_failure
DHCLIENT_RETRY_ON_FAILURE = 0
DHCP_CLIENTS = {c.__name__: c for c in [dhclient, udhcpc]}

_modinfo = {
'mhelp': 'setting for dhcp client',
'attrs': {
'dhcp-client': {
'help': 'Name of dhcp client in use',
'validvals': list(DHCP_CLIENTS.values()),
'required': False,
'exemple': ['dhcp-client dhclient'],
'default': 'dhclient',
},
}
}

_policies = (
'dhcp-wait',
'dhcp6-duid',
'dhcp6-ll-wait',
'dhclient_retry_on_failure',
'udhcpc-wait-timeout',
)

def client_factory(self, **kwargs):
def init_client(ifaceobj):
client_name = self.get_param('dhcp-client', ifaceobj)
if client_name not in self.DHCP_CLIENTS:
self.logger.error(f"dhcp: {client_name} not found, falling back to dhclient")
client_name = 'dhclient'
cls = self.DHCP_CLIENTS[client_name]
return cls(**kwargs)
return init_client

def __init__(self, *args, **kargs):
Addon.__init__(self)
moduleBase.__init__(self, *args, **kargs)
self.dhclientcmd = dhclient(**kargs)
self.init_client = self.client_factory(**kargs)
self.dhcp_client = None
vrf_id = self._get_vrf_context()
if vrf_id and vrf_id == 'mgmt':
self.mgmt_vrf_context = True
Expand Down Expand Up @@ -123,7 +158,7 @@ def dhclient_check(self, ifname, family, ip_config_before, retry, dhclient_cmd_p
"%s: dhclient: couldn't detect new ip address, retrying %s more times..."
% (ifname, retry)
)
self.dhclientcmd.stop(ifname)
self.dhcp_client.stop(ifname)
else:
self.logger.error("%s: dhclient: timeout failed to detect new ip addresses" % ifname)
return -1
Expand All @@ -132,8 +167,8 @@ def dhclient_check(self, ifname, family, ip_config_before, retry, dhclient_cmd_p

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)
dhclient4_running = self.dhcp_client.is_running(ifaceobj.name)
dhclient6_running = self.dhcp_client.is_running6(ifaceobj.name)

# 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
Expand Down Expand Up @@ -175,14 +210,14 @@ def _up(self, ifaceobj):
# First release any existing dhclient processes
try:
if not ifupdownflags.flags.PERFMODE:
self.dhclientcmd.stop(ifaceobj.name)
self.dhcp_client.stop(ifaceobj.name)
except Exception:
pass

self.dhclient_start_and_check(
ifaceobj.name,
"inet",
self.dhclientcmd.start,
self.dhcp_client.start,
wait=wait,
cmd_prefix=dhclient_cmd_prefix
)
Expand All @@ -203,7 +238,7 @@ def _up(self, ifaceobj):
self.sysctl_set('net.ipv6.conf.%s' %ifaceobj.name +
'.autoconf', autoconf)
try:
self.dhclientcmd.stop6(ifaceobj.name, duid=dhcp6_duid)
self.dhcp_client.stop6(ifaceobj.name, duid=dhcp6_duid)
except Exception:
pass
#add delay before starting IPv6 dhclient to
Expand All @@ -215,7 +250,7 @@ def _up(self, ifaceobj):
%(utils.ip_cmd, ifaceobj.name))
r = re.search('inet6 .* scope link', addr_output)
if r:
self.dhclientcmd.start6(ifaceobj.name,
self.dhcp_client.start6(ifaceobj.name,
wait=wait,
cmd_prefix=dhclient_cmd_prefix, duid=dhcp6_duid)
return
Expand Down Expand Up @@ -246,10 +281,10 @@ def _dhcp_down(self, ifaceobj):
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.dhcp_client.release6(ifaceobj.name, dhclient_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.dhcp_client.release(ifaceobj.name, dhclient_cmd_prefix)
self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET)

def _down(self, ifaceobj):
Expand All @@ -260,8 +295,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.dhcp_client.is_running(ifaceobjcurr.name)
dhcp_v6 = self.dhcp_client.is_running6(ifaceobjcurr.name)

if dhcp_v4:
dhcp_running = True
Expand All @@ -282,10 +317,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.dhcp_client.is_running(ifaceobjrunning.name):
ifaceobjrunning.addr_family.append('inet')
ifaceobjrunning.addr_method = 'dhcp'
if self.dhclientcmd.is_running6(ifaceobjrunning.name):
if self.dhcp_client.is_running6(ifaceobjrunning.name):
ifaceobjrunning.addr_family.append('inet6')
ifaceobjrunning.addr_method = 'dhcp6'

Expand Down Expand Up @@ -316,6 +351,7 @@ def run(self, ifaceobj, operation, query_ifaceobj=None, **extra_args):
of interfaces. status is success if the running state is same
as user required state in ifaceobj. error otherwise.
"""
self.dhcp_client = self.init_client(ifaceobj)
op_handler = self._run_ops.get(operation)
if not op_handler:
return
Expand Down
9 changes: 8 additions & 1 deletion ifupdown2/ifupdown/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import subprocess
import itertools

from functools import partial
from functools import partial, reduce
from ipaddress import IPv4Address

try:
Expand Down Expand Up @@ -575,4 +575,11 @@ def get_vni_mcastgrp_in_map(cls, vni_mcastgrp_map):
raise
return vnid

@staticmethod
def dig(data, *args, default=None):
NotFoundDict = type('NoneDict', (dict,), {})
ret = reduce(lambda d, key: d.get(key, NotFoundDict()), args, data)
return default if isinstance(ret, NotFoundDict) else ret


fcntl.fcntl(utils.DEVNULL, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
21 changes: 2 additions & 19 deletions ifupdown2/ifupdownaddons/dhclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#

import os
import errno

try:
from ifupdown2.ifupdown.utils import utils
Expand All @@ -19,27 +18,11 @@ 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)
return self.pid_exists(f'/run/dhclient.{ifacename}.pid', 'dhclient')

def is_running6(self, ifacename):
return self._pid_exists('/run/dhclient6.%s.pid' %ifacename)
return self.pid_exists(f'/run/dhclient6.{ifacename}.pid', 'dhclient')

def _run_dhclient_cmd(self, cmd, cmd_prefix=None):
if not cmd_prefix:
Expand Down
29 changes: 24 additions & 5 deletions ifupdown2/ifupdownaddons/modulebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,19 @@ def merge_modinfo_with_policy_files(self):
trying to "inject" new attributes to prevent breakages and security issue
"""
attrs = dict(self.get_modinfo().get('attrs', {}))
policies = getattr(self, '_policies', [])

if not attrs:
return

error_msg = 'this attribute doesn\'t exist or isn\'t supported'

# first check module_defaults
for key, value in list(policymanager.policymanager_api.get_module_defaults(self.modulename).items()):
if not key in attrs:
self.logger.warning('%s: %s: %s' % (self.modulename, key, error_msg))
continue
attrs[key]['default'] = value
for key, value in policymanager.policymanager_api.get_module_defaults(self.modulename).items():
if key in attrs:
attrs[key]['default'] = value
elif key not in policies:
self.logger.warning(f'{self.modulename}: {key} {error_msg}')

# then check module_globals (overrides module_defaults)
policy_modinfo = policymanager.policymanager_api.get_module_globals(self.modulename, '_modinfo')
Expand Down Expand Up @@ -518,3 +519,21 @@ def _get_vlan_id(self, ifaceobj):
return -1

return self._get_vlan_id_from_ifacename(ifaceobj.name)

def get_param(self, attrname, ifaceobj=None, module_name=None):
"""
Get a parameter with the following priority (first is higher):
* first iface attribue value
* default policy
* modinfo default attribute
"""
module_name = module_name or self.__class__.__name__
ifname = ifaceobj.name if ifaceobj else None
modinfo = getattr(self, '_modinfo', {})
policy_api = policymanager.policymanager_api
values = [
ifaceobj.get_attr_value_first(attrname) if ifaceobj else None,
policy_api.get_iface_default(module_name, ifname, attrname),
utils.dig(modinfo, 'attrs', attrname, 'default'),
]
return next((v for v in values if v is not None), None)
Loading