summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrett Holman <brett.holman@canonical.com>2023-04-19 16:12:55 -0600
committerGitHub <noreply@github.com>2023-04-19 16:12:55 -0600
commit5942f4023e2581a43f31d547995095ca49954353 (patch)
tree510009a412671c4adb041c05fc803db1ea4e8c22
parent9e4cb4f06b2e362889bcbda77ef6fad52afed52b (diff)
downloadcloud-init-git-5942f4023e2581a43f31d547995095ca49954353.tar.gz
[1/2] DHCP: Refactor dhcp client code (#2122)
Move isc-dhclient code to dhcp.py In support of the upcoming deprecation of isc-dhcp-client, this code refactors current dhcp code into classes in dhcp.py. The primary user-visible change should be the addition of the following log: dhcp.py[DEBUG]: DHCP client selected: dhclient This code lays groundwork to enable alternate implementations to live side by side in the codebase to be selected with distro-defined priority fallback. Note that maybe_perform_dhcp_discovery() now selects which dhcp client to call, and then runs the corresponding client's dhcp_discovery() method. Currently only class IscDhclient is implemented, however a yet-to-be-implemented class Dhcpcd exists to test fallback behavior and this will be implemented in part two of this series. Part of this refactor includes shifting dhclient service management from hardcoded calls to the distro-defined manage_service() method in the *BSDs. Future work is required in this area to support multiple clients via select_dhcp_client().
-rw-r--r--cloudinit/distros/__init__.py21
-rw-r--r--cloudinit/distros/alpine.py10
-rw-r--r--cloudinit/distros/freebsd.py14
-rw-r--r--cloudinit/distros/openbsd.py10
-rw-r--r--cloudinit/net/dhcp.py616
-rw-r--r--cloudinit/net/ephemeral.py16
-rw-r--r--cloudinit/net/freebsd.py15
-rw-r--r--cloudinit/net/openbsd.py4
-rw-r--r--cloudinit/sources/DataSourceAzure.py4
-rw-r--r--cloudinit/sources/DataSourceCloudStack.py61
-rw-r--r--cloudinit/sources/DataSourceEc2.py4
-rw-r--r--cloudinit/sources/DataSourceGCE.py3
-rw-r--r--cloudinit/sources/DataSourceHetzner.py4
-rw-r--r--cloudinit/sources/DataSourceNWCS.py1
-rw-r--r--cloudinit/sources/DataSourceOpenStack.py5
-rw-r--r--cloudinit/sources/DataSourceOracle.py3
-rw-r--r--cloudinit/sources/DataSourceScaleway.py1
-rw-r--r--cloudinit/sources/DataSourceUpCloud.py5
-rw-r--r--cloudinit/sources/DataSourceVultr.py14
-rw-r--r--cloudinit/sources/helpers/vmware/imc/config_nic.py10
-rw-r--r--cloudinit/sources/helpers/vultr.py11
-rw-r--r--tests/unittests/config/test_cc_ntp.py7
-rw-r--r--tests/unittests/config/test_cc_puppet.py4
-rw-r--r--tests/unittests/config/test_cc_set_passwords.py16
-rw-r--r--tests/unittests/distros/test_manage_service.py26
-rw-r--r--tests/unittests/net/test_dhcp.py144
-rw-r--r--tests/unittests/net/test_ephemeral.py8
-rw-r--r--tests/unittests/sources/test_azure.py17
-rw-r--r--tests/unittests/sources/test_cloudstack.py21
-rw-r--r--tests/unittests/sources/test_ec2.py5
-rw-r--r--tests/unittests/sources/test_hetzner.py1
-rw-r--r--tests/unittests/sources/test_nwcs.py1
-rw-r--r--tests/unittests/sources/test_openstack.py2
-rw-r--r--tests/unittests/sources/test_oracle.py5
-rw-r--r--tests/unittests/sources/test_upcloud.py5
-rw-r--r--tests/unittests/sources/test_vultr.py5
-rw-r--r--tests/unittests/util.py3
37 files changed, 623 insertions, 479 deletions
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index b82852e1..ec148939 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -32,7 +32,7 @@ from cloudinit import (
from cloudinit.distros.networking import LinuxNetworking, Networking
from cloudinit.distros.parsers import hosts
from cloudinit.features import ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES
-from cloudinit.net import activators, eni, network_state, renderers
+from cloudinit.net import activators, dhcp, eni, network_state, renderers
from cloudinit.net.network_state import parse_net_config_data
from cloudinit.net.renderer import Renderer
@@ -110,12 +110,14 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
resolve_conf_fn = "/etc/resolv.conf"
osfamily: str
+ dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd]
def __init__(self, name, cfg, paths):
self._paths = paths
self._cfg = cfg
self.name = name
self.networking: Networking = self.networking_cls()
+ self.dhcp_client_priority = [dhcp.IscDhclient, dhcp.Dhcpcd]
def _unpickle(self, ci_pkl_version: int) -> None:
"""Perform deserialization fixes for Distro."""
@@ -185,7 +187,8 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
self._write_hostname(writeable_hostname, self.hostname_conf_fn)
self._apply_hostname(writeable_hostname)
- def uses_systemd(self):
+ @staticmethod
+ def uses_systemd():
"""Wrapper to report whether this distro uses systemd or sysvinit."""
return uses_systemd()
@@ -916,15 +919,18 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
args.append(message)
return args
- def manage_service(self, action: str, service: str):
+ @classmethod
+ def manage_service(
+ cls, action: str, service: str, *extra_args: str, rcs=None
+ ):
"""
Perform the requested action on a service. This handles the common
'systemctl' and 'service' cases and may be overridden in subclasses
as necessary.
May raise ProcessExecutionError
"""
- init_cmd = self.init_cmd
- if self.uses_systemd() or "systemctl" in init_cmd:
+ init_cmd = cls.init_cmd
+ if cls.uses_systemd() or "systemctl" in init_cmd:
init_cmd = ["systemctl"]
cmds = {
"stop": ["stop", service],
@@ -948,7 +954,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
"status": [service, "status"],
}
cmd = list(init_cmd) + list(cmds[action])
- return subp.subp(cmd, capture=True)
+ return subp.subp(cmd, capture=True, rcs=rcs)
def set_keymap(self, layout, model, variant, options):
if self.uses_systemd():
@@ -1193,6 +1199,3 @@ def uses_systemd():
return stat.S_ISDIR(res.st_mode)
except Exception:
return False
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/distros/alpine.py b/cloudinit/distros/alpine.py
index 4a23fe07..53eebb08 100644
--- a/cloudinit/distros/alpine.py
+++ b/cloudinit/distros/alpine.py
@@ -173,13 +173,17 @@ class Distro(distros.Distro):
return command
- def uses_systemd(self):
+ @staticmethod
+ def uses_systemd():
"""
Alpine uses OpenRC, not systemd
"""
return False
- def manage_service(self, action: str, service: str):
+ @classmethod
+ def manage_service(
+ self, action: str, service: str, *extra_args: str, rcs=None
+ ):
"""
Perform the requested action on a service. This handles OpenRC
specific implementation details.
@@ -202,4 +206,4 @@ class Distro(distros.Distro):
"status": list(init_cmd) + [service, "status"],
}
cmd = list(cmds[action])
- return subp.subp(cmd, capture=True)
+ return subp.subp(cmd, capture=True, rcs=rcs)
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index 4268abe6..77a94c61 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -37,14 +37,17 @@ class Distro(cloudinit.distros.bsd.BSD):
prefer_fqdn = True # See rc.conf(5) in FreeBSD
home_dir = "/usr/home"
- def manage_service(self, action: str, service: str):
+ @classmethod
+ def manage_service(
+ cls, action: str, service: str, *extra_args: str, rcs=None
+ ):
"""
Perform the requested action on a service. This handles FreeBSD's
'service' case. The FreeBSD 'service' is closer in features to
'systemctl' than SysV init's 'service', so we override it.
May raise ProcessExecutionError
"""
- init_cmd = self.init_cmd
+ init_cmd = cls.init_cmd
cmds = {
"stop": [service, "stop"],
"start": [service, "start"],
@@ -55,8 +58,8 @@ class Distro(cloudinit.distros.bsd.BSD):
"try-reload": [service, "restart"],
"status": [service, "status"],
}
- cmd = list(init_cmd) + list(cmds[action])
- return subp.subp(cmd, capture=True)
+ cmd = init_cmd + cmds[action] + list(extra_args)
+ return subp.subp(cmd, capture=True, rcs=rcs)
def _get_add_member_to_group_cmd(self, member_name, group_name):
return ["pw", "usermod", "-n", member_name, "-G", group_name]
@@ -191,6 +194,3 @@ class Distro(cloudinit.distros.bsd.BSD):
["update"],
freq=PER_INSTANCE,
)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/distros/openbsd.py b/cloudinit/distros/openbsd.py
index 72e9bc45..5950c9db 100644
--- a/cloudinit/distros/openbsd.py
+++ b/cloudinit/distros/openbsd.py
@@ -25,13 +25,14 @@ class Distro(cloudinit.distros.netbsd.NetBSD):
def _get_add_member_to_group_cmd(self, member_name, group_name):
return ["usermod", "-G", group_name, member_name]
- def manage_service(self, action: str, service: str):
+ @classmethod
+ def manage_service(cls, action: str, service: str, *extra_args, rcs=None):
"""
Perform the requested action on a service. This handles OpenBSD's
'rcctl'.
May raise ProcessExecutionError
"""
- init_cmd = self.init_cmd
+ init_cmd = cls.init_cmd
cmds = {
"stop": ["stop", service],
"start": ["start", service],
@@ -43,7 +44,7 @@ class Distro(cloudinit.distros.netbsd.NetBSD):
"status": ["check", service],
}
cmd = list(init_cmd) + list(cmds[action])
- return subp.subp(cmd, capture=True)
+ return subp.subp(cmd, capture=True, rcs=rcs)
def lock_passwd(self, name):
try:
@@ -59,6 +60,3 @@ class Distro(cloudinit.distros.netbsd.NetBSD):
"""Return env vars used in OpenBSD package_command operations"""
e = os.environ.copy()
return e
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index c934ee16..6c8c2f54 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -4,13 +4,16 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+import abc
import contextlib
+import glob
import logging
import os
import re
import signal
import time
from io import StringIO
+from typing import Any, Dict, List
import configobj
@@ -47,7 +50,22 @@ class NoDHCPLeaseMissingDhclientError(NoDHCPLeaseError):
"""Raised when unable to find dhclient."""
-def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None):
+def select_dhcp_client(distro):
+ """distros set priority list, select based on this order which to use
+
+ If the priority dhcp client isn't found, fall back to lower in list.
+ """
+ for client in distro.dhcp_client_priority:
+ try:
+ dhcp_client = client()
+ LOG.debug("DHCP client selected: %s", client.client_name)
+ return dhcp_client
+ except NoDHCPLeaseMissingDhclientError:
+ LOG.warning("DHCP client not found: %s", client.client_name)
+ raise NoDHCPLeaseMissingDhclientError()
+
+
+def maybe_perform_dhcp_discovery(distro, nic=None, dhcp_log_func=None):
"""Perform dhcp discovery if nic valid and dhclient command exists.
If the nic is invalid or undiscoverable or dhclient command is not found,
@@ -70,161 +88,8 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None):
"Skip dhcp_discovery: nic %s not found in get_devicelist.", nic
)
raise NoDHCPLeaseInterfaceError()
- dhclient_path = subp.which("dhclient")
- if not dhclient_path:
- LOG.debug("Skip dhclient configuration: No dhclient command found.")
- raise NoDHCPLeaseMissingDhclientError()
- return dhcp_discovery(dhclient_path, nic, dhcp_log_func)
-
-
-def parse_dhcp_lease_file(lease_file):
- """Parse the given dhcp lease file for the most recent lease.
-
- Return a list of dicts of dhcp options. Each dict contains key value pairs
- a specific lease in order from oldest to newest.
-
- @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile
- content.
- """
- lease_regex = re.compile(r"lease {(?P<lease>.*?)}\n", re.DOTALL)
- dhcp_leases = []
- lease_content = util.load_file(lease_file)
- if len(lease_content) == 0:
- raise InvalidDHCPLeaseFileError(
- "Cannot parse empty dhcp lease file {0}".format(lease_file)
- )
- for lease in lease_regex.findall(lease_content):
- lease_options = []
- for line in lease.split(";"):
- # Strip newlines, double-quotes and option prefix
- line = line.strip().replace('"', "").replace("option ", "")
- if not line:
- continue
- lease_options.append(line.split(" ", 1))
- dhcp_leases.append(dict(lease_options))
- if not dhcp_leases:
- raise InvalidDHCPLeaseFileError(
- "Cannot parse dhcp lease file {0}. No leases found".format(
- lease_file
- )
- )
- return dhcp_leases
-
-
-def dhcp_discovery(dhclient_cmd_path, interface, dhcp_log_func=None):
- """Run dhclient on the interface without scripts or filesystem artifacts.
-
- @param dhclient_cmd_path: Full path to the dhclient used.
- @param interface: Name of the network interface on which to dhclient.
- @param dhcp_log_func: A callable accepting the dhclient output and error
- streams.
-
- @return: A list of dicts of representing the dhcp leases parsed from the
- dhclient.lease file or empty list.
- """
- LOG.debug("Performing a dhcp discovery on %s", interface)
-
- # We want to avoid running /sbin/dhclient-script because of side-effects in
- # /etc/resolv.conf any any other vendor specific scripts in
- # /etc/dhcp/dhclient*hooks.d.
- pid_file = "/run/dhclient.pid"
- lease_file = "/run/dhclient.lease"
-
- # this function waits for these files to exist, clean previous runs
- # to avoid false positive in wait_for_files
- with contextlib.suppress(FileNotFoundError):
- os.remove(pid_file)
- os.remove(lease_file)
-
- # ISC dhclient needs the interface up to send initial discovery packets.
- # Generally dhclient relies on dhclient-script PREINIT action to bring the
- # link up before attempting discovery. Since we are using -sf /bin/true,
- # we need to do that "link up" ourselves first.
- subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True)
- # For INFINIBAND port the dhlient must be sent with dhcp-client-identifier.
- # So here we are checking if the interface is INFINIBAND or not.
- # If yes, we are generating the the client-id to be used with the dhclient
- cmd = [
- dhclient_cmd_path,
- "-1",
- "-v",
- "-lf",
- lease_file,
- "-pf",
- pid_file,
- interface,
- "-sf",
- "/bin/true",
- ]
- if is_ib_interface(interface):
- dhcp_client_identifier = "20:%s" % get_interface_mac(interface)[36:]
- interface_dhclient_content = (
- 'interface "%s" '
- "{send dhcp-client-identifier %s;}"
- % (interface, dhcp_client_identifier)
- )
- tmp_dir = temp_utils.get_tmp_ancestor(needs_exe=True)
- file_name = os.path.join(tmp_dir, interface + "-dhclient.conf")
- util.write_file(file_name, interface_dhclient_content)
- cmd.append("-cf")
- cmd.append(file_name)
-
- try:
- out, err = subp.subp(cmd, capture=True)
- except subp.ProcessExecutionError as error:
- LOG.debug(
- "dhclient exited with code: %s stderr: %r stdout: %r",
- error.exit_code,
- error.stderr,
- error.stdout,
- )
- raise NoDHCPLeaseError from error
-
- # Wait for pid file and lease file to appear, and for the process
- # named by the pid file to daemonize (have pid 1 as its parent). If we
- # try to read the lease file before daemonization happens, we might try
- # to read it before the dhclient has actually written it. We also have
- # to wait until the dhclient has become a daemon so we can be sure to
- # kill the correct process, thus freeing cleandir to be deleted back
- # up the callstack.
- missing = util.wait_for_files(
- [pid_file, lease_file], maxwait=5, naplen=0.01
- )
- if missing:
- LOG.warning(
- "dhclient did not produce expected files: %s",
- ", ".join(os.path.basename(f) for f in missing),
- )
- return []
-
- ppid = "unknown"
- daemonized = False
- for _ in range(0, 1000):
- pid_content = util.load_file(pid_file).strip()
- try:
- pid = int(pid_content)
- except ValueError:
- pass
- else:
- ppid = util.get_proc_ppid(pid)
- if ppid == 1:
- LOG.debug("killing dhclient with pid=%s", pid)
- os.kill(pid, signal.SIGKILL)
- daemonized = True
- break
- time.sleep(0.01)
-
- if not daemonized:
- LOG.error(
- "dhclient(pid=%s, parentpid=%s) failed to daemonize after %s "
- "seconds",
- pid_content,
- ppid,
- 0.01 * 1000,
- )
- if dhcp_log_func is not None:
- dhcp_log_func(out, err)
- return parse_dhcp_lease_file(lease_file)
+ client = select_dhcp_client(distro)
+ return client.dhcp_discovery(nic, dhcp_log_func, distro)
def networkd_parse_lease(content):
@@ -267,101 +132,368 @@ def networkd_get_option_from_leases(keyname, leases_d=None):
return None
-def parse_static_routes(rfc3442):
- """parse rfc3442 format and return a list containing tuple of strings.
+class DhcpClient(abc.ABC):
+ client_name = ""
+
+ @classmethod
+ def kill_dhcp_client(cls):
+ subp.subp(["pkill", cls.client_name], rcs=[0, 1])
- The tuple is composed of the network_address (including net length) and
- gateway for a parsed static route. It can parse two formats of rfc3442,
- one from dhcpcd and one from dhclient (isc).
+ @classmethod
+ def clear_leases(cls):
+ cls.kill_dhcp_client()
+ files = glob.glob("/var/lib/dhcp/*")
+ for file in files:
+ os.remove(file)
- @param rfc3442: string in rfc3442 format (isc or dhcpd)
- @returns: list of tuple(str, str) for all valid parsed routes until the
- first parsing error.
+ @classmethod
+ def start_service(cls, dhcp_interface: str, distro):
+ distro.manage_service(
+ "start", cls.client_name, dhcp_interface, rcs=[0, 1]
+ )
- E.g.
- sr=parse_static_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1")
- sr=[
- ("169.254.169.254/32", "130.56.248.255"), ("0.0.0.0/0", "130.56.240.1")
- ]
+ @classmethod
+ def stop_service(cls, dhcp_interface: str, distro):
+ distro.manage_service("stop", cls.client_name, rcs=[0, 1])
- sr2 = parse_static_routes("24.191.168.128 192.168.128.1,0 192.168.128.1")
- sr2 = [
- ("191.168.128.0/24", "192.168.128.1"), ("0.0.0.0/0", "192.168.128.1")
- ]
- Python version of isc-dhclient's hooks:
- /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes
- """
- # raw strings from dhcp lease may end in semi-colon
- rfc3442 = rfc3442.rstrip(";")
- tokens = [tok for tok in re.split(r"[, .]", rfc3442) if tok]
- static_routes = []
-
- def _trunc_error(cidr, required, remain):
- msg = (
- "RFC3442 string malformed. Current route has CIDR of %s "
- "and requires %s significant octets, but only %s remain. "
- "Verify DHCP rfc3442-classless-static-routes value: %s"
- % (cidr, required, remain, rfc3442)
+class IscDhclient(DhcpClient):
+ client_name = "dhclient"
+
+ def __init__(self):
+ self.dhclient_path = subp.which("dhclient")
+ if not self.dhclient_path:
+ LOG.debug(
+ "Skip dhclient configuration: No dhclient command found."
+ )
+ raise NoDHCPLeaseMissingDhclientError()
+
+ @staticmethod
+ def parse_dhcp_lease_file(lease_file: str) -> List[Dict[str, Any]]:
+ """Parse the given dhcp lease file returning all leases as dicts.
+
+ Return a list of dicts of dhcp options. Each dict contains key value
+ pairs a specific lease in order from oldest to newest.
+
+ @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile
+ content.
+ """
+ lease_regex = re.compile(r"lease {(?P<lease>.*?)}\n", re.DOTALL)
+ dhcp_leases = []
+ lease_content = util.load_file(lease_file)
+ if len(lease_content) == 0:
+ raise InvalidDHCPLeaseFileError(
+ "Cannot parse empty dhcp lease file {0}".format(lease_file)
+ )
+ for lease in lease_regex.findall(lease_content):
+ lease_options = []
+ for line in lease.split(";"):
+ # Strip newlines, double-quotes and option prefix
+ line = line.strip().replace('"', "").replace("option ", "")
+ if not line:
+ continue
+ lease_options.append(line.split(" ", 1))
+ dhcp_leases.append(dict(lease_options))
+ if not dhcp_leases:
+ raise InvalidDHCPLeaseFileError(
+ "Cannot parse dhcp lease file {0}. No leases found".format(
+ lease_file
+ )
+ )
+ return dhcp_leases
+
+ def dhcp_discovery(
+ self,
+ interface,
+ dhcp_log_func=None,
+ distro=None,
+ ):
+ """Run dhclient on the interface without scripts/filesystem artifacts.
+
+ @param dhclient_cmd_path: Full path to the dhclient used.
+ @param interface: Name of the network interface on which to dhclient.
+ @param dhcp_log_func: A callable accepting the dhclient output and
+ error streams.
+
+ @return: A list of dicts of representing the dhcp leases parsed from
+ the dhclient.lease file or empty list.
+ """
+ LOG.debug("Performing a dhcp discovery on %s", interface)
+
+ # We want to avoid running /sbin/dhclient-script because of
+ # side-effects in # /etc/resolv.conf any any other vendor specific
+ # scripts in /etc/dhcp/dhclient*hooks.d.
+ pid_file = "/run/dhclient.pid"
+ lease_file = "/run/dhclient.lease"
+
+ # this function waits for these files to exist, clean previous runs
+ # to avoid false positive in wait_for_files
+ with contextlib.suppress(FileNotFoundError):
+ os.remove(pid_file)
+ os.remove(lease_file)
+
+ # ISC dhclient needs the interface up to send initial discovery packets
+ # Generally dhclient relies on dhclient-script PREINIT action to bring
+ # the link up before attempting discovery. Since we are using
+ # -sf /bin/true, we need to do that "link up" ourselves first.
+ subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True)
+ # For INFINIBAND port the dhlient must be sent with
+ # dhcp-client-identifier. So here we are checking if the interface is
+ # INFINIBAND or not. If yes, we are generating the the client-id to be
+ # used with the dhclient
+ cmd = [
+ self.dhclient_path,
+ "-1",
+ "-v",
+ "-lf",
+ lease_file,
+ "-pf",
+ pid_file,
+ interface,
+ "-sf",
+ "/bin/true",
+ ]
+ if is_ib_interface(interface):
+ dhcp_client_identifier = (
+ "20:%s" % get_interface_mac(interface)[36:]
+ )
+ interface_dhclient_content = (
+ 'interface "%s" '
+ "{send dhcp-client-identifier %s;}"
+ % (interface, dhcp_client_identifier)
+ )
+ tmp_dir = temp_utils.get_tmp_ancestor(needs_exe=True)
+ file_name = os.path.join(tmp_dir, interface + "-dhclient.conf")
+ util.write_file(file_name, interface_dhclient_content)
+ cmd.append("-cf")
+ cmd.append(file_name)
+
+ try:
+ out, err = subp.subp(cmd, capture=True)
+ except subp.ProcessExecutionError as error:
+ LOG.debug(
+ "dhclient exited with code: %s stderr: %r stdout: %r",
+ error.exit_code,
+ error.stderr,
+ error.stdout,
+ )
+ raise NoDHCPLeaseError from error
+
+ # Wait for pid file and lease file to appear, and for the process
+ # named by the pid file to daemonize (have pid 1 as its parent). If we
+ # try to read the lease file before daemonization happens, we might try
+ # to read it before the dhclient has actually written it. We also have
+ # to wait until the dhclient has become a daemon so we can be sure to
+ # kill the correct process, thus freeing cleandir to be deleted back
+ # up the callstack.
+ missing = util.wait_for_files(
+ [pid_file, lease_file], maxwait=5, naplen=0.01
)
- LOG.error(msg)
-
- current_idx = 0
- for idx, tok in enumerate(tokens):
- if idx < current_idx:
- continue
- net_length = int(tok)
- if net_length in range(25, 33):
- req_toks = 9
- if len(tokens[idx:]) < req_toks:
- _trunc_error(net_length, req_toks, len(tokens[idx:]))
- return static_routes
- net_address = ".".join(tokens[idx + 1 : idx + 5])
- gateway = ".".join(tokens[idx + 5 : idx + req_toks])
- current_idx = idx + req_toks
- elif net_length in range(17, 25):
- req_toks = 8
- if len(tokens[idx:]) < req_toks:
- _trunc_error(net_length, req_toks, len(tokens[idx:]))
- return static_routes
- net_address = ".".join(tokens[idx + 1 : idx + 4] + ["0"])
- gateway = ".".join(tokens[idx + 4 : idx + req_toks])
- current_idx = idx + req_toks
- elif net_length in range(9, 17):
- req_toks = 7
- if len(tokens[idx:]) < req_toks:
- _trunc_error(net_length, req_toks, len(tokens[idx:]))
- return static_routes
- net_address = ".".join(tokens[idx + 1 : idx + 3] + ["0", "0"])
- gateway = ".".join(tokens[idx + 3 : idx + req_toks])
- current_idx = idx + req_toks
- elif net_length in range(1, 9):
- req_toks = 6
- if len(tokens[idx:]) < req_toks:
- _trunc_error(net_length, req_toks, len(tokens[idx:]))
- return static_routes
- net_address = ".".join(tokens[idx + 1 : idx + 2] + ["0", "0", "0"])
- gateway = ".".join(tokens[idx + 2 : idx + req_toks])
- current_idx = idx + req_toks
- elif net_length == 0:
- req_toks = 5
- if len(tokens[idx:]) < req_toks:
- _trunc_error(net_length, req_toks, len(tokens[idx:]))
- return static_routes
- net_address = "0.0.0.0"
- gateway = ".".join(tokens[idx + 1 : idx + req_toks])
- current_idx = idx + req_toks
- else:
+ if missing:
+ LOG.warning(
+ "dhclient did not produce expected files: %s",
+ ", ".join(os.path.basename(f) for f in missing),
+ )
+ return []
+
+ ppid = "unknown"
+ daemonized = False
+ for _ in range(0, 1000):
+ pid_content = util.load_file(pid_file).strip()
+ try:
+ pid = int(pid_content)
+ except ValueError:
+ pass
+ else:
+ ppid = util.get_proc_ppid(pid)
+ if ppid == 1:
+ LOG.debug("killing dhclient with pid=%s", pid)
+ os.kill(pid, signal.SIGKILL)
+ daemonized = True
+ break
+ time.sleep(0.01)
+
+ if not daemonized:
LOG.error(
- 'Parsed invalid net length "%s". Verify DHCP '
- "rfc3442-classless-static-routes value.",
- net_length,
+ "dhclient(pid=%s, parentpid=%s) failed to daemonize after %s "
+ "seconds",
+ pid_content,
+ ppid,
+ 0.01 * 1000,
)
- return static_routes
+ if dhcp_log_func is not None:
+ dhcp_log_func(out, err)
+ return self.parse_dhcp_lease_file(lease_file)
+
+ @staticmethod
+ def parse_static_routes(rfc3442):
+ """
+ parse rfc3442 format and return a list containing tuple of strings.
+
+ The tuple is composed of the network_address (including net length) and
+ gateway for a parsed static route. It can parse two formats of
+ rfc3442, one from dhcpcd and one from dhclient (isc).
+
+ @param rfc3442: string in rfc3442 format (isc or dhcpd)
+ @returns: list of tuple(str, str) for all valid parsed routes until the
+ first parsing error.
+
+ e.g.:
+
+ sr=parse_static_routes(\
+ "32,169,254,169,254,130,56,248,255,0,130,56,240,1")
+ sr=[
+ ("169.254.169.254/32", "130.56.248.255"), \
+ ("0.0.0.0/0", "130.56.240.1")
+ ]
+
+ sr2 = parse_static_routes(\
+ "24.191.168.128 192.168.128.1,0 192.168.128.1")
+ sr2 = [
+ ("191.168.128.0/24", "192.168.128.1"),\
+ ("0.0.0.0/0", "192.168.128.1")
+ ]
+
+ Python version of isc-dhclient's hooks:
+ /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes
+ """
+ # raw strings from dhcp lease may end in semi-colon
+ rfc3442 = rfc3442.rstrip(";")
+ tokens = [tok for tok in re.split(r"[, .]", rfc3442) if tok]
+ static_routes = []
+
+ def _trunc_error(cidr, required, remain):
+ msg = (
+ "RFC3442 string malformed. Current route has CIDR of %s "
+ "and requires %s significant octets, but only %s remain. "
+ "Verify DHCP rfc3442-classless-static-routes value: %s"
+ % (cidr, required, remain, rfc3442)
+ )
+ LOG.error(msg)
- static_routes.append(("%s/%s" % (net_address, net_length), gateway))
+ current_idx = 0
+ for idx, tok in enumerate(tokens):
+ if idx < current_idx:
+ continue
+ net_length = int(tok)
+ if net_length in range(25, 33):
+ req_toks = 9
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx + 1 : idx + 5])
+ gateway = ".".join(tokens[idx + 5 : idx + req_toks])
+ current_idx = idx + req_toks
+ elif net_length in range(17, 25):
+ req_toks = 8
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx + 1 : idx + 4] + ["0"])
+ gateway = ".".join(tokens[idx + 4 : idx + req_toks])
+ current_idx = idx + req_toks
+ elif net_length in range(9, 17):
+ req_toks = 7
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx + 1 : idx + 3] + ["0", "0"])
+ gateway = ".".join(tokens[idx + 3 : idx + req_toks])
+ current_idx = idx + req_toks
+ elif net_length in range(1, 9):
+ req_toks = 6
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(
+ tokens[idx + 1 : idx + 2] + ["0", "0", "0"]
+ )
+ gateway = ".".join(tokens[idx + 2 : idx + req_toks])
+ current_idx = idx + req_toks
+ elif net_length == 0:
+ req_toks = 5
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = "0.0.0.0"
+ gateway = ".".join(tokens[idx + 1 : idx + req_toks])
+ current_idx = idx + req_toks
+ else:
+ LOG.error(
+ 'Parsed invalid net length "%s". Verify DHCP '
+ "rfc3442-classless-static-routes value.",
+ net_length,
+ )
+ return static_routes
- return static_routes
+ static_routes.append(
+ ("%s/%s" % (net_address, net_length), gateway)
+ )
+ return static_routes
+
+ @staticmethod
+ def get_dhclient_d():
+ # find lease files directory
+ supported_dirs = [
+ "/var/lib/dhclient",
+ "/var/lib/dhcp",
+ "/var/lib/NetworkManager",
+ ]
+ for d in supported_dirs:
+ if os.path.exists(d) and len(os.listdir(d)) > 0:
+ LOG.debug("Using %s lease directory", d)
+ return d
+ return None
+
+ @staticmethod
+ def get_latest_lease(lease_d=None):
+ # find latest lease file
+ if lease_d is None:
+ lease_d = IscDhclient.get_dhclient_d()
+ if not lease_d:
+ return None
+ lease_files = os.listdir(lease_d)
+ latest_mtime = -1
+ latest_file = None
+
+ # lease files are named inconsistently across distros.
+ # We assume that 'dhclient6' indicates ipv6 and ignore it.
+ # ubuntu:
+ # dhclient.<iface>.leases, dhclient.leases, dhclient6.leases
+ # centos6:
+ # dhclient-<iface>.leases, dhclient6.leases
+ # centos7: ('--' is not a typo)
+ # dhclient--<iface>.lease, dhclient6.leases
+ for fname in lease_files:
+ if fname.startswith("dhclient6"):
+ # avoid files that start with dhclient6 assuming dhcpv6.
+ continue
+ if not (fname.endswith(".lease") or fname.endswith(".leases")):
+ continue
-# vi: ts=4 expandtab
+ abs_path = os.path.join(lease_d, fname)
+ mtime = os.path.getmtime(abs_path)
+ if mtime > latest_mtime:
+ latest_mtime = mtime
+ latest_file = abs_path
+ return latest_file
+
+ @staticmethod
+ def parse_dhcp_server_from_lease_file(lease_file):
+ with open(lease_file, "r") as fd:
+ for line in fd:
+ if "dhcp-server-identifier" in line:
+ words = line.strip(" ;\r\n").split(" ")
+ if len(words) > 2:
+ dhcptok = words[2]
+ LOG.debug("Found DHCP identifier %s", dhcptok)
+ latest_address = dhcptok
+ return latest_address
+
+
+class Dhcpcd:
+ client_name = "dhcpcd"
+
+ def __init__(self):
+ raise NoDHCPLeaseMissingDhclientError("Dhcpcd not yet implemented")
diff --git a/cloudinit/net/ephemeral.py b/cloudinit/net/ephemeral.py
index 1dfd1c42..130afa17 100644
--- a/cloudinit/net/ephemeral.py
+++ b/cloudinit/net/ephemeral.py
@@ -9,9 +9,9 @@ from typing import Any, Dict, List, Optional
import cloudinit.net as net
from cloudinit import subp
from cloudinit.net.dhcp import (
+ IscDhclient,
NoDHCPLeaseError,
maybe_perform_dhcp_discovery,
- parse_static_routes,
)
LOG = logging.getLogger(__name__)
@@ -305,6 +305,7 @@ class EphemeralIPv6Network:
class EphemeralDHCPv4:
def __init__(
self,
+ distro,
iface=None,
connectivity_url_data: Optional[Dict[str, Any]] = None,
dhcp_log_func=None,
@@ -314,6 +315,7 @@ class EphemeralDHCPv4:
self.lease = None
self.dhcp_log_func = dhcp_log_func
self.connectivity_url_data = connectivity_url_data
+ self.distro = distro
def __enter__(self):
"""Setup sandboxed dhcp context, unless connectivity_url can already be
@@ -351,7 +353,9 @@ class EphemeralDHCPv4:
"""
if self.lease:
return self.lease
- leases = maybe_perform_dhcp_discovery(self.iface, self.dhcp_log_func)
+ leases = maybe_perform_dhcp_discovery(
+ self.distro, self.iface, self.dhcp_log_func
+ )
if not leases:
raise NoDHCPLeaseError()
self.lease = leases[-1]
@@ -378,7 +382,7 @@ class EphemeralDHCPv4:
kwargs["prefix_or_mask"], kwargs["ip"]
)
if kwargs["static_routes"]:
- kwargs["static_routes"] = parse_static_routes(
+ kwargs["static_routes"] = IscDhclient.parse_static_routes(
kwargs["static_routes"]
)
if self.connectivity_url_data:
@@ -412,6 +416,7 @@ class EphemeralIPNetwork:
def __init__(
self,
+ distro,
interface,
ipv6: bool = False,
ipv4: bool = True,
@@ -421,13 +426,16 @@ class EphemeralIPNetwork:
self.ipv6 = ipv6
self.stack = contextlib.ExitStack()
self.state_msg: str = ""
+ self.distro = distro
def __enter__(self):
# ipv6 dualstack might succeed when dhcp4 fails
# therefore catch exception unless only v4 is used
try:
if self.ipv4:
- self.stack.enter_context(EphemeralDHCPv4(self.interface))
+ self.stack.enter_context(
+ EphemeralDHCPv4(self.distro, self.interface)
+ )
if self.ipv6:
self.stack.enter_context(EphemeralIPv6Network(self.interface))
# v6 link local might be usable
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
index 415f4a5a..38038e3e 100644
--- a/cloudinit/net/freebsd.py
+++ b/cloudinit/net/freebsd.py
@@ -1,8 +1,9 @@
# This file is part of cloud-init. See LICENSE file for license information.
import cloudinit.net.bsd
+from cloudinit import distros
from cloudinit import log as logging
-from cloudinit import subp, util
+from cloudinit import net, subp, util
LOG = logging.getLogger(__name__)
@@ -50,10 +51,8 @@ class Renderer(cloudinit.net.bsd.BSDRenderer):
for dhcp_interface in self.dhcp_interfaces():
# Observed on DragonFlyBSD 6. If we use the "restart" parameter,
# the routes are not recreated.
- subp.subp(
- ["service", "dhclient", "stop", dhcp_interface],
- rcs=[0, 1],
- capture=True,
+ net.dhcp.IscDhclient.stop_service(
+ dhcp_interface, distros.freebsd.Distro
)
subp.subp(["service", "netif", "restart"], capture=True)
@@ -66,10 +65,8 @@ class Renderer(cloudinit.net.bsd.BSDRenderer):
subp.subp(["service", "routing", "restart"], capture=True, rcs=[0, 1])
for dhcp_interface in self.dhcp_interfaces():
- subp.subp(
- ["service", "dhclient", "start", dhcp_interface],
- rcs=[0, 1],
- capture=True,
+ net.dhcp.IscDhclient.start_service(
+ dhcp_interface, distros.freebsd.Distro
)
def set_route(self, network, netmask, gateway):
diff --git a/cloudinit/net/openbsd.py b/cloudinit/net/openbsd.py
index 70e9f461..5dd13800 100644
--- a/cloudinit/net/openbsd.py
+++ b/cloudinit/net/openbsd.py
@@ -4,7 +4,7 @@ import platform
import cloudinit.net.bsd
from cloudinit import log as logging
-from cloudinit import subp, util
+from cloudinit import net, subp, util
LOG = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ class Renderer(cloudinit.net.bsd.BSDRenderer):
["dhcpleasectl", "-w", "30", interface], capture=True
)
else:
- subp.subp(["pkill", "dhclient"], capture=True, rcs=[0, 1])
+ net.dhcp.IscDhclient.kill_dhcp_client()
subp.subp(["route", "del", "default"], capture=True, rcs=[0, 1])
subp.subp(["route", "flush", "default"], capture=True, rcs=[0, 1])
subp.subp(["sh", "/etc/netstart"], capture=True)
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 83dbdce1..a2b052b7 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -384,6 +384,7 @@ class DataSourceAzure(sources.DataSource):
LOG.debug("Requested ephemeral networking (iface=%s)", iface)
self._ephemeral_dhcp_ctx = EphemeralDHCPv4(
+ self.distro,
iface=iface,
dhcp_log_func=dhcp_log_cb,
)
@@ -1942,6 +1943,3 @@ datasources = [
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 3cdfeeca..86124e16 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -207,53 +207,6 @@ def get_default_gateway():
return None
-def get_dhclient_d():
- # find lease files directory
- supported_dirs = [
- "/var/lib/dhclient",
- "/var/lib/dhcp",
- "/var/lib/NetworkManager",
- ]
- for d in supported_dirs:
- if os.path.exists(d) and len(os.listdir(d)) > 0:
- LOG.debug("Using %s lease directory", d)
- return d
- return None
-
-
-def get_latest_lease(lease_d=None):
- # find latest lease file
- if lease_d is None:
- lease_d = get_dhclient_d()
- if not lease_d:
- return None
- lease_files = os.listdir(lease_d)
- latest_mtime = -1
- latest_file = None
-
- # lease files are named inconsistently across distros.
- # We assume that 'dhclient6' indicates ipv6 and ignore it.
- # ubuntu:
- # dhclient.<iface>.leases, dhclient.leases, dhclient6.leases
- # centos6:
- # dhclient-<iface>.leases, dhclient6.leases
- # centos7: ('--' is not a typo)
- # dhclient--<iface>.lease, dhclient6.leases
- for fname in lease_files:
- if fname.startswith("dhclient6"):
- # avoid files that start with dhclient6 assuming dhcpv6.
- continue
- if not (fname.endswith(".lease") or fname.endswith(".leases")):
- continue
-
- abs_path = os.path.join(lease_d, fname)
- mtime = os.path.getmtime(abs_path)
- if mtime > latest_mtime:
- latest_mtime = mtime
- latest_file = abs_path
- return latest_file
-
-
def get_vr_address():
# Get the address of the virtual router via dhcp leases
# If no virtual router is detected, fallback on default gateway.
@@ -277,19 +230,12 @@ def get_vr_address():
return latest_address
# Try dhcp lease files next...
- lease_file = get_latest_lease()
+ lease_file = dhcp.IscDhclient.get_latest_lease()
if not lease_file:
LOG.debug("No lease file found, using default gateway")
return get_default_gateway()
- with open(lease_file, "r") as fd:
- for line in fd:
- if "dhcp-server-identifier" in line:
- words = line.strip(" ;\r\n").split(" ")
- if len(words) > 2:
- dhcptok = words[2]
- LOG.debug("Found DHCP identifier %s", dhcptok)
- latest_address = dhcptok
+ lease_file = dhcp.IscDhclient.parse_dhcp_server_from_lease_file(lease_file)
if not latest_address:
# No virtual router found, fallback on default gateway
LOG.debug("No DHCP found, using default gateway")
@@ -306,6 +252,3 @@ datasources = [
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index e8fb0023..2143873a 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -131,6 +131,7 @@ class DataSourceEc2(sources.DataSource):
return False
try:
with EphemeralIPNetwork(
+ self.distro,
self.fallback_interface,
ipv4=True,
ipv6=True,
@@ -1019,6 +1020,3 @@ datasources = [
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py
index bb44cd1f..041c8914 100644
--- a/cloudinit/sources/DataSourceGCE.py
+++ b/cloudinit/sources/DataSourceGCE.py
@@ -84,6 +84,7 @@ class DataSourceGCE(sources.DataSource):
network_context = noop()
if self.perform_dhcp_setup:
network_context = EphemeralDHCPv4(
+ self.distro,
self.fallback_interface,
)
with network_context:
@@ -353,5 +354,3 @@ if __name__ == "__main__":
data["user-data-b64"] = b64encode(data["user-data"]).decode()
print(json.dumps(data, indent=1, sort_keys=True, separators=(",", ": ")))
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceHetzner.py b/cloudinit/sources/DataSourceHetzner.py
index 14f14677..e20af1c3 100644
--- a/cloudinit/sources/DataSourceHetzner.py
+++ b/cloudinit/sources/DataSourceHetzner.py
@@ -57,6 +57,7 @@ class DataSourceHetzner(sources.DataSource):
try:
with EphemeralDHCPv4(
+ self.distro,
iface=net.find_fallback_nic(),
connectivity_url_data={
"url": BASE_URL_V1 + "/metadata/instance-id",
@@ -159,6 +160,3 @@ datasources = [
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceNWCS.py b/cloudinit/sources/DataSourceNWCS.py
index aebbf689..a147613d 100644
--- a/cloudinit/sources/DataSourceNWCS.py
+++ b/cloudinit/sources/DataSourceNWCS.py
@@ -66,6 +66,7 @@ class DataSourceNWCS(sources.DataSource):
LOG.info("Attempting to get metadata via DHCP")
with EphemeralDHCPv4(
+ self.distro,
iface=net.find_fallback_nic(),
connectivity_url_data={
"url": BASE_URL_V1 + "/metadata/instance-id",
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index c480b627..5fb5d839 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -154,7 +154,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
if self.perform_dhcp_setup: # Setup networking in init-local stage.
try:
- with EphemeralDHCPv4(self.fallback_interface):
+ with EphemeralDHCPv4(self.distro, self.fallback_interface):
results = util.log_time(
logfunc=LOG.debug,
msg="Crawl of metadata service",
@@ -290,6 +290,3 @@ datasources = [
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
index 3baf06e1..7d464580 100644
--- a/cloudinit/sources/DataSourceOracle.py
+++ b/cloudinit/sources/DataSourceOracle.py
@@ -150,6 +150,7 @@ class DataSourceOracle(sources.DataSource):
self.system_uuid = _read_system_uuid()
network_context = ephemeral.EphemeralDHCPv4(
+ self.distro,
iface=net.find_fallback_nic(),
connectivity_url_data={
"url": METADATA_PATTERN.format(version=2, path="instance"),
@@ -409,5 +410,3 @@ if __name__ == "__main__":
}
)
)
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py
index 5c420398..e7fb9a3b 100644
--- a/cloudinit/sources/DataSourceScaleway.py
+++ b/cloudinit/sources/DataSourceScaleway.py
@@ -280,6 +280,7 @@ class DataSourceScaleway(sources.DataSource):
# before giving up. Lower it in config file and try it first as
# it will only reach timeout on VMs with only IPv6 addresses.
with EphemeralDHCPv4(
+ self.distro,
self._fallback_interface,
) as ipv4:
util.log_time(
diff --git a/cloudinit/sources/DataSourceUpCloud.py b/cloudinit/sources/DataSourceUpCloud.py
index 43122f0b..908df5c6 100644
--- a/cloudinit/sources/DataSourceUpCloud.py
+++ b/cloudinit/sources/DataSourceUpCloud.py
@@ -71,7 +71,7 @@ class DataSourceUpCloud(sources.DataSource):
LOG.debug("Finding a fallback NIC")
nic = cloudnet.find_fallback_nic()
LOG.debug("Discovering metadata via DHCP interface %s", nic)
- with EphemeralDHCPv4(nic):
+ with EphemeralDHCPv4(self.distro, nic):
md = util.log_time(
logfunc=LOG.debug,
msg="Reading from metadata service",
@@ -160,6 +160,3 @@ datasources = [
# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceVultr.py b/cloudinit/sources/DataSourceVultr.py
index f7c56780..7b7cc696 100644
--- a/cloudinit/sources/DataSourceVultr.py
+++ b/cloudinit/sources/DataSourceVultr.py
@@ -7,7 +7,7 @@
import cloudinit.sources.helpers.vultr as vultr
from cloudinit import log as log
-from cloudinit import sources, util, version
+from cloudinit import sources, stages, util, version
LOG = log.getLogger(__name__)
BUILTIN_DS_CONFIG = {
@@ -88,6 +88,7 @@ class DataSourceVultr(sources.DataSource):
# Get the metadata by flag
def get_metadata(self):
return vultr.get_metadata(
+ self.distro,
self.ds_cfg["url"],
self.ds_cfg["timeout"],
self.ds_cfg["retries"],
@@ -136,7 +137,16 @@ if __name__ == "__main__":
print("Machine is not a Vultr instance")
sys.exit(1)
+ # It should probably be safe to try to detect distro via stages.Init(),
+ # which will fall back to Ubuntu if no distro config is found.
+ # this distro object is only used for dhcp fallback. Feedback from user(s)
+ # of __main__ would help determine if a better approach exists.
+ #
+ # we don't needReportEventStack, so reporter=True
+ distro = stages.Init(reporter=True).distro
+
md = vultr.get_metadata(
+ distro,
BUILTIN_DS_CONFIG["url"],
BUILTIN_DS_CONFIG["timeout"],
BUILTIN_DS_CONFIG["retries"],
@@ -148,5 +158,3 @@ if __name__ == "__main__":
print(util.json_dumps(sysinfo))
print(util.json_dumps(config))
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
index ba2488be..b07214a2 100644
--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
@@ -9,7 +9,7 @@ import logging
import os
import re
-from cloudinit import subp, util
+from cloudinit import net, subp, util
from cloudinit.net.network_state import ipv4_mask_to_net_prefix
logger = logging.getLogger(__name__)
@@ -245,10 +245,7 @@ class NicConfigurator:
def clear_dhcp(self):
logger.info("Clearing DHCP leases")
-
- # Ignore the return code 1.
- subp.subp(["pkill", "dhclient"], rcs=[0, 1])
- subp.subp(["rm", "-f", "/var/lib/dhcp/*"])
+ net.dhcp.IscDhclient.clear_leases()
def configure(self, osfamily=None):
"""
@@ -280,6 +277,3 @@ class NicConfigurator:
util.write_file(interfaceFile, content="\n".join(lines))
self.clear_dhcp()
-
-
-# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vultr.py b/cloudinit/sources/helpers/vultr.py
index a6d5cea7..71676bb1 100644
--- a/cloudinit/sources/helpers/vultr.py
+++ b/cloudinit/sources/helpers/vultr.py
@@ -18,7 +18,9 @@ LOG = log.getLogger(__name__)
@lru_cache()
-def get_metadata(url, timeout, retries, sec_between, agent, tmp_dir=None):
+def get_metadata(
+ distro, url, timeout, retries, sec_between, agent, tmp_dir=None
+):
# Bring up interface (and try untill one works)
exception = RuntimeError("Failed to DHCP")
@@ -26,7 +28,9 @@ def get_metadata(url, timeout, retries, sec_between, agent, tmp_dir=None):
for iface in get_interface_list():
try:
with EphemeralDHCPv4(
- iface=iface, connectivity_url_data={"url": url}
+ distro,
+ iface=iface,
+ connectivity_url_data={"url": url},
):
# Check for the metadata route, skip if not there
if not check_route(url):
@@ -285,6 +289,3 @@ def add_interface_names(netcfg):
% interface["mac_address"]
)
interface["name"] = interface_name
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/config/test_cc_ntp.py b/tests/unittests/config/test_cc_ntp.py
index f8b71d2b..62c9b3fb 100644
--- a/tests/unittests/config/test_cc_ntp.py
+++ b/tests/unittests/config/test_cc_ntp.py
@@ -503,7 +503,9 @@ class TestNtp(FilesystemMockingTestCase):
m_util.is_FreeBSD.return_value = is_FreeBSD
m_util.is_OpenBSD.return_value = is_OpenBSD
cc_ntp.handle("notimportant", cfg, mycloud, None)
- m_subp.assert_called_with(expected_service_call, capture=True)
+ m_subp.assert_called_with(
+ expected_service_call, capture=True, rcs=None
+ )
self.assertEqual(expected_content, util.load_file(confpath))
@@ -837,6 +839,3 @@ class TestNTPSchema:
else:
with pytest.raises(SchemaValidationError, match=error_msg):
validate_cloudconfig_schema(config, get_schema(), strict=True)
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/config/test_cc_puppet.py b/tests/unittests/config/test_cc_puppet.py
index 9c55e9b5..c60988e4 100644
--- a/tests/unittests/config/test_cc_puppet.py
+++ b/tests/unittests/config/test_cc_puppet.py
@@ -38,6 +38,7 @@ class TestManagePuppetServices(CiTestCase):
mock.call(
["systemctl", "enable", "puppet-agent.service"],
capture=True,
+ rcs=None,
)
]
self.assertIn(expected_calls, m_subp.call_args_list)
@@ -51,6 +52,7 @@ class TestManagePuppetServices(CiTestCase):
mock.call(
["systemctl", "start", "puppet-agent.service"],
capture=True,
+ rcs=None,
)
]
self.assertIn(expected_calls, m_subp.call_args_list)
@@ -62,10 +64,12 @@ class TestManagePuppetServices(CiTestCase):
mock.call(
["systemctl", "enable", "puppet-agent.service"],
capture=True,
+ rcs=None,
),
mock.call(
["systemctl", "enable", "puppet.service"],
capture=True,
+ rcs=None,
),
]
self.assertEqual(expected_calls, m_subp.call_args_list)
diff --git a/tests/unittests/config/test_cc_set_passwords.py b/tests/unittests/config/test_cc_set_passwords.py
index f6885b2b..1a9fcd3c 100644
--- a/tests/unittests/config/test_cc_set_passwords.py
+++ b/tests/unittests/config/test_cc_set_passwords.py
@@ -20,8 +20,12 @@ LOG = logging.getLogger(__name__)
SYSTEMD_CHECK_CALL = mock.call(
["systemctl", "show", "--property", "ActiveState", "--value", "ssh"]
)
-SYSTEMD_RESTART_CALL = mock.call(["systemctl", "restart", "ssh"], capture=True)
-SERVICE_RESTART_CALL = mock.call(["service", "ssh", "restart"], capture=True)
+SYSTEMD_RESTART_CALL = mock.call(
+ ["systemctl", "restart", "ssh"], capture=True, rcs=None
+)
+SERVICE_RESTART_CALL = mock.call(
+ ["service", "ssh", "restart"], capture=True, rcs=None
+)
@pytest.fixture(autouse=True)
@@ -46,7 +50,6 @@ class TestHandleSSHPwauth:
(True, True, "activating"),
(True, True, "inactive"),
(True, False, None),
- (False, True, None),
(False, False, None),
),
)
@@ -79,10 +82,6 @@ class TestHandleSSHPwauth:
assert SYSTEMD_RESTART_CALL in m_subp.call_args_list
else:
assert SYSTEMD_RESTART_CALL not in m_subp.call_args_list
- else:
- assert SERVICE_RESTART_CALL in m_subp.call_args_list
- assert SYSTEMD_CHECK_CALL not in m_subp.call_args_list
- assert SYSTEMD_RESTART_CALL not in m_subp.call_args_list
@mock.patch(f"{MODPATH}update_ssh_config", return_value=True)
@mock.patch("cloudinit.distros.subp.subp")
@@ -728,6 +727,3 @@ class TestSetPasswordsSchema:
def test_schema_validation(self, config, expectation):
with expectation:
validate_cloudconfig_schema(config, get_schema(), strict=True)
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/distros/test_manage_service.py b/tests/unittests/distros/test_manage_service.py
index 98823770..d7637d38 100644
--- a/tests/unittests/distros/test_manage_service.py
+++ b/tests/unittests/distros/test_manage_service.py
@@ -13,13 +13,13 @@ class TestManageService(CiTestCase):
super(TestManageService, self).setUp()
self.dist = MockDistro()
- @mock.patch.object(MockDistro, "uses_systemd", return_value=False)
+ @mock.patch.object(MockDistro, "uses_systemd", return_value=True)
@mock.patch("cloudinit.distros.subp.subp")
def test_manage_service_systemctl_initcmd(self, m_subp, m_sysd):
self.dist.init_cmd = ["systemctl"]
self.dist.manage_service("start", "myssh")
m_subp.assert_called_with(
- ["systemctl", "start", "myssh"], capture=True
+ ["systemctl", "start", "myssh"], capture=True, rcs=None
)
@mock.patch.object(MockDistro, "uses_systemd", return_value=False)
@@ -27,7 +27,9 @@ class TestManageService(CiTestCase):
def test_manage_service_service_initcmd(self, m_subp, m_sysd):
self.dist.init_cmd = ["service"]
self.dist.manage_service("start", "myssh")
- m_subp.assert_called_with(["service", "myssh", "start"], capture=True)
+ m_subp.assert_called_with(
+ ["service", "myssh", "start"], capture=True, rcs=None
+ )
@mock.patch.object(MockDistro, "uses_systemd", return_value=False)
@mock.patch("cloudinit.distros.subp.subp")
@@ -36,7 +38,9 @@ class TestManageService(CiTestCase):
dist.init_cmd = ["rc-service", "--nocolor"]
dist.manage_service("start", "myssh")
m_subp.assert_called_with(
- ["rc-service", "--nocolor", "myssh", "start"], capture=True
+ ["rc-service", "--nocolor", "myssh", "start"],
+ capture=True,
+ rcs=None,
)
@mock.patch("cloudinit.distros.subp.subp")
@@ -45,7 +49,7 @@ class TestManageService(CiTestCase):
dist.update_cmd = ["rc-update", "--nocolor"]
dist.manage_service("enable", "myssh")
m_subp.assert_called_with(
- ["rc-update", "--nocolor", "add", "myssh"], capture=True
+ ["rc-update", "--nocolor", "add", "myssh"], capture=True, rcs=None
)
@mock.patch("cloudinit.distros.subp.subp")
@@ -53,14 +57,18 @@ class TestManageService(CiTestCase):
dist = _get_distro("openbsd")
dist.init_cmd = ["rcctl"]
dist.manage_service("start", "myssh")
- m_subp.assert_called_with(["rcctl", "start", "myssh"], capture=True)
+ m_subp.assert_called_with(
+ ["rcctl", "start", "myssh"], capture=True, rcs=None
+ )
@mock.patch("cloudinit.distros.subp.subp")
def test_manage_service_fbsd_service_initcmd(self, m_subp):
dist = _get_distro("freebsd")
dist.init_cmd = ["service"]
dist.manage_service("enable", "myssh")
- m_subp.assert_called_with(["service", "myssh", "enable"], capture=True)
+ m_subp.assert_called_with(
+ ["service", "myssh", "enable"], capture=True, rcs=None
+ )
@mock.patch.object(MockDistro, "uses_systemd", return_value=True)
@mock.patch("cloudinit.distros.subp.subp")
@@ -68,7 +76,7 @@ class TestManageService(CiTestCase):
self.dist.init_cmd = ["ignore"]
self.dist.manage_service("start", "myssh")
m_subp.assert_called_with(
- ["systemctl", "start", "myssh"], capture=True
+ ["systemctl", "start", "myssh"], capture=True, rcs=None
)
@mock.patch.object(MockDistro, "uses_systemd", return_value=True)
@@ -77,5 +85,5 @@ class TestManageService(CiTestCase):
self.dist.init_cmd = ["ignore"]
self.dist.manage_service("disable", "myssh")
m_subp.assert_called_with(
- ["systemctl", "disable", "myssh"], capture=True
+ ["systemctl", "disable", "myssh"], capture=True, rcs=None
)
diff --git a/tests/unittests/net/test_dhcp.py b/tests/unittests/net/test_dhcp.py
index a55d49cb..ed01e60b 100644
--- a/tests/unittests/net/test_dhcp.py
+++ b/tests/unittests/net/test_dhcp.py
@@ -9,14 +9,12 @@ import responses
from cloudinit.net.dhcp import (
InvalidDHCPLeaseFileError,
+ IscDhclient,
NoDHCPLeaseError,
NoDHCPLeaseInterfaceError,
NoDHCPLeaseMissingDhclientError,
- dhcp_discovery,
maybe_perform_dhcp_discovery,
networkd_load_leases,
- parse_dhcp_lease_file,
- parse_static_routes,
)
from cloudinit.net.ephemeral import EphemeralDHCPv4
from cloudinit.util import ensure_file, subp, write_file
@@ -26,6 +24,7 @@ from tests.unittests.helpers import (
mock,
populate_dir,
)
+from tests.unittests.util import MockDistro
PID_F = "/run/dhclient.pid"
LEASE_F = "/run/dhclient.lease"
@@ -38,21 +37,25 @@ class TestParseDHCPLeasesFile(CiTestCase):
empty_file = self.tmp_path("leases")
ensure_file(empty_file)
with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager:
- parse_dhcp_lease_file(empty_file)
+ IscDhclient.parse_dhcp_lease_file(empty_file)
error = context_manager.exception
self.assertIn("Cannot parse empty dhcp lease file", str(error))
def test_parse_malformed_lease_file_content_errors(self):
- """parse_dhcp_lease_file errors when file content isn't dhcp leases."""
+ """IscDhclient.parse_dhcp_lease_file errors when file content isn't
+ dhcp leases.
+ """
non_lease_file = self.tmp_path("leases")
write_file(non_lease_file, "hi mom.")
with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager:
- parse_dhcp_lease_file(non_lease_file)
+ IscDhclient.parse_dhcp_lease_file(non_lease_file)
error = context_manager.exception
self.assertIn("Cannot parse dhcp lease file", str(error))
def test_parse_multiple_leases(self):
- """parse_dhcp_lease_file returns a list of all leases within."""
+ """IscDhclient.parse_dhcp_lease_file returns a list of all leases
+ within.
+ """
lease_file = self.tmp_path("leases")
content = dedent(
"""
@@ -93,12 +96,16 @@ class TestParseDHCPLeasesFile(CiTestCase):
},
]
write_file(lease_file, content)
- self.assertCountEqual(expected, parse_dhcp_lease_file(lease_file))
+ self.assertCountEqual(
+ expected, IscDhclient.parse_dhcp_lease_file(lease_file)
+ )
class TestDHCPRFC3442(CiTestCase):
def test_parse_lease_finds_rfc3442_classless_static_routes(self):
- """parse_dhcp_lease_file returns rfc3442-classless-static-routes."""
+ """IscDhclient.parse_dhcp_lease_file returns
+ rfc3442-classless-static-routes.
+ """
lease_file = self.tmp_path("leases")
content = dedent(
"""
@@ -125,11 +132,13 @@ class TestDHCPRFC3442(CiTestCase):
}
]
write_file(lease_file, content)
- self.assertCountEqual(expected, parse_dhcp_lease_file(lease_file))
+ self.assertCountEqual(
+ expected, IscDhclient.parse_dhcp_lease_file(lease_file)
+ )
def test_parse_lease_finds_classless_static_routes(self):
"""
- parse_dhcp_lease_file returns classless-static-routes
+ IscDhclient.parse_dhcp_lease_file returns classless-static-routes
for Centos lease format.
"""
lease_file = self.tmp_path("leases")
@@ -158,7 +167,9 @@ class TestDHCPRFC3442(CiTestCase):
}
]
write_file(lease_file, content)
- self.assertCountEqual(expected, parse_dhcp_lease_file(lease_file))
+ self.assertCountEqual(
+ expected, IscDhclient.parse_dhcp_lease_file(lease_file)
+ )
@mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network")
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
@@ -176,7 +187,9 @@ class TestDHCPRFC3442(CiTestCase):
}
]
m_maybe.return_value = lease
- eph = EphemeralDHCPv4()
+ eph = EphemeralDHCPv4(
+ MockDistro(),
+ )
eph.obtain_lease()
expected_kwargs = {
"interface": "wlp3s0",
@@ -207,7 +220,9 @@ class TestDHCPRFC3442(CiTestCase):
}
]
m_maybe.return_value = lease
- eph = EphemeralDHCPv4()
+ eph = EphemeralDHCPv4(
+ MockDistro(),
+ )
eph.obtain_lease()
expected_kwargs = {
"interface": "wlp3s0",
@@ -223,41 +238,43 @@ class TestDHCPRFC3442(CiTestCase):
class TestDHCPParseStaticRoutes(CiTestCase):
with_logs = True
- def parse_static_routes_empty_string(self):
- self.assertEqual([], parse_static_routes(""))
+ def test_parse_static_routes_empty_string(self):
+ self.assertEqual([], IscDhclient.parse_static_routes(""))
def test_parse_static_routes_invalid_input_returns_empty_list(self):
rfc3442 = "32,169,254,169,254,130,56,248"
- self.assertEqual([], parse_static_routes(rfc3442))
+ self.assertEqual([], IscDhclient.parse_static_routes(rfc3442))
def test_parse_static_routes_bogus_width_returns_empty_list(self):
rfc3442 = "33,169,254,169,254,130,56,248"
- self.assertEqual([], parse_static_routes(rfc3442))
+ self.assertEqual([], IscDhclient.parse_static_routes(rfc3442))
def test_parse_static_routes_single_ip(self):
rfc3442 = "32,169,254,169,254,130,56,248,255"
self.assertEqual(
[("169.254.169.254/32", "130.56.248.255")],
- parse_static_routes(rfc3442),
+ IscDhclient.parse_static_routes(rfc3442),
)
def test_parse_static_routes_single_ip_handles_trailing_semicolon(self):
rfc3442 = "32,169,254,169,254,130,56,248,255;"
self.assertEqual(
[("169.254.169.254/32", "130.56.248.255")],
- parse_static_routes(rfc3442),
+ IscDhclient.parse_static_routes(rfc3442),
)
def test_parse_static_routes_default_route(self):
rfc3442 = "0,130,56,240,1"
self.assertEqual(
- [("0.0.0.0/0", "130.56.240.1")], parse_static_routes(rfc3442)
+ [("0.0.0.0/0", "130.56.240.1")],
+ IscDhclient.parse_static_routes(rfc3442),
)
def test_unspecified_gateway(self):
rfc3442 = "32,169,254,169,254,0,0,0,0"
self.assertEqual(
- [("169.254.169.254/32", "0.0.0.0")], parse_static_routes(rfc3442)
+ [("169.254.169.254/32", "0.0.0.0")],
+ IscDhclient.parse_static_routes(rfc3442),
)
def test_parse_static_routes_class_c_b_a(self):
@@ -273,7 +290,7 @@ class TestDHCPParseStaticRoutes(CiTestCase):
("10.0.0.0/8", "10.0.0.4"),
]
),
- sorted(parse_static_routes(rfc3442)),
+ sorted(IscDhclient.parse_static_routes(rfc3442)),
)
def test_parse_static_routes_logs_error_truncated(self):
@@ -285,7 +302,7 @@ class TestDHCPParseStaticRoutes(CiTestCase):
"netlen": "33,0",
}
for rfc3442 in bad_rfc3442.values():
- self.assertEqual([], parse_static_routes(rfc3442))
+ self.assertEqual([], IscDhclient.parse_static_routes(rfc3442))
logs = self.logs.getvalue()
self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines()))
@@ -302,7 +319,7 @@ class TestDHCPParseStaticRoutes(CiTestCase):
("172.16.0.0/16", "172.16.0.4"),
]
),
- sorted(parse_static_routes(rfc3442)),
+ sorted(IscDhclient.parse_static_routes(rfc3442)),
)
logs = self.logs.getvalue()
@@ -317,7 +334,7 @@ class TestDHCPParseStaticRoutes(CiTestCase):
("0.0.0.0/0", "192.168.128.1"),
]
),
- sorted(parse_static_routes(redhat_format)),
+ sorted(IscDhclient.parse_static_routes(redhat_format)),
)
def test_redhat_format_with_a_space_too_much_after_comma(self):
@@ -329,7 +346,7 @@ class TestDHCPParseStaticRoutes(CiTestCase):
("0.0.0.0/0", "192.168.128.1"),
]
),
- sorted(parse_static_routes(redhat_format)),
+ sorted(IscDhclient.parse_static_routes(redhat_format)),
)
@@ -343,7 +360,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
m_fallback_nic.return_value = None # No fallback nic found
with pytest.raises(NoDHCPLeaseInterfaceError):
- maybe_perform_dhcp_discovery()
+ maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
"Skip dhcp_discovery: Unable to find fallback nic.",
@@ -364,10 +381,34 @@ class TestDHCPDiscoveryClean(CiTestCase):
]
with pytest.raises(NoDHCPLeaseError):
- maybe_perform_dhcp_discovery()
+ maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
- "dhclient exited with code: -5",
+ "DHCP client selected: dhclient",
+ self.logs.getvalue(),
+ )
+
+ @mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
+ @mock.patch("cloudinit.net.dhcp.os.remove")
+ @mock.patch("cloudinit.net.dhcp.subp.subp")
+ @mock.patch("cloudinit.net.dhcp.subp.which")
+ def test_dhcp_client_failover(self, m_which, m_subp, m_remove, m_fallback):
+ """Log and do nothing when nic is absent and no fallback is found."""
+ m_subp.side_effect = [
+ ("", ""),
+ subp.ProcessExecutionError(exit_code=-5),
+ ]
+
+ m_which.side_effect = [False, True]
+ with pytest.raises(NoDHCPLeaseError):
+ maybe_perform_dhcp_discovery(MockDistro())
+
+ self.assertIn(
+ "DHCP client not found: dhclient",
+ self.logs.getvalue(),
+ )
+ self.assertIn(
+ "DHCP client not found: dhcpcd",
self.logs.getvalue(),
)
@@ -375,7 +416,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
def test_provided_nic_does_not_exist(self, m_fallback_nic):
"""When the provided nic doesn't exist, log a message and no-op."""
with pytest.raises(NoDHCPLeaseInterfaceError):
- maybe_perform_dhcp_discovery("idontexist")
+ maybe_perform_dhcp_discovery(MockDistro(), "idontexist")
self.assertIn(
"Skip dhcp_discovery: nic idontexist not found in get_devicelist.",
@@ -390,7 +431,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
m_which.return_value = None # dhclient isn't found
with pytest.raises(NoDHCPLeaseMissingDhclientError):
- maybe_perform_dhcp_discovery()
+ maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
"Skip dhclient configuration: No dhclient command found.",
@@ -434,11 +475,11 @@ class TestDHCPDiscoveryClean(CiTestCase):
"routers": "192.168.2.1",
}
],
- parse_dhcp_lease_file("lease"),
+ IscDhclient.parse_dhcp_lease_file("lease"),
)
with self.assertRaises(InvalidDHCPLeaseFileError):
with mock.patch("cloudinit.util.load_file", return_value=""):
- dhcp_discovery(DHCLIENT, "eth9")
+ IscDhclient().dhcp_discovery("eth9")
self.assertIn(
"dhclient(pid=, parentpid=unknown) failed "
"to daemonize after 10.0 seconds",
@@ -460,7 +501,9 @@ class TestDHCPDiscoveryClean(CiTestCase):
# Don't create pid or leases file
m_wait.return_value = [PID_F] # Return the missing pidfile wait for
m_getppid.return_value = 1 # Indicate that dhclient has daemonized
- self.assertEqual([], dhcp_discovery("/sbin/dhclient", "eth9"))
+ self.assertEqual(
+ [], IscDhclient().dhcp_discovery("/sbin/dhclient", "eth9")
+ )
self.assertEqual(
mock.call([PID_F, LEASE_F], maxwait=5, naplen=0.01),
m_wait.call_args_list[0],
@@ -476,10 +519,12 @@ class TestDHCPDiscoveryClean(CiTestCase):
@mock.patch("cloudinit.net.dhcp.util.get_proc_ppid")
@mock.patch("cloudinit.net.dhcp.os.kill")
@mock.patch("cloudinit.net.dhcp.subp.subp")
+ @mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.util.wait_for_files", return_value=False)
def test_dhcp_discovery(
self,
m_wait,
+ m_which,
m_subp,
m_kill,
m_getppid,
@@ -516,7 +561,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
"routers": "192.168.2.1",
}
],
- dhcp_discovery("/sbin/dhclient", "eth9"),
+ IscDhclient().dhcp_discovery("eth9"),
)
# Interface was brought up before dhclient called
m_subp.assert_has_calls(
@@ -554,12 +599,14 @@ class TestDHCPDiscoveryClean(CiTestCase):
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.util.get_proc_ppid", return_value=1)
@mock.patch("cloudinit.net.dhcp.os.kill")
+ @mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.net.dhcp.subp.subp", return_value=("", ""))
@mock.patch("cloudinit.util.wait_for_files", return_value=False)
def test_dhcp_discovery_ib(
self,
m_wait,
m_subp,
+ m_which,
m_kill,
m_getppid,
m_remove,
@@ -595,7 +642,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
"routers": "192.168.2.1",
}
],
- dhcp_discovery("/sbin/dhclient", "ib0"),
+ IscDhclient().dhcp_discovery("ib0"),
)
# Interface was brought up before dhclient called
m_subp.assert_has_calls(
@@ -667,7 +714,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
self.assertEqual(out, dhclient_out)
self.assertEqual(err, dhclient_err)
- dhcp_discovery(DHCLIENT, "eth9", dhcp_log_func=dhcp_log_func)
+ IscDhclient().dhcp_discovery("eth9", dhcp_log_func=dhcp_log_func)
class TestSystemdParseLeases(CiTestCase):
@@ -796,6 +843,7 @@ class TestEphemeralDhcpNoNetworkSetup(ResponsesTestCase):
self.responses.add(responses.GET, url)
with EphemeralDHCPv4(
+ MockDistro(),
connectivity_url_data={"url": url},
) as lease:
self.assertIsNone(lease)
@@ -819,6 +867,7 @@ class TestEphemeralDhcpNoNetworkSetup(ResponsesTestCase):
self.responses.add(responses.GET, url, body=b"", status=404)
with EphemeralDHCPv4(
+ MockDistro(),
connectivity_url_data={"url": url},
) as lease:
self.assertEqual(fake_lease, lease)
@@ -840,7 +889,9 @@ class TestEphemeralDhcpLeaseErrors:
m_dhcp.side_effect = [error_class()]
with pytest.raises(error_class):
- EphemeralDHCPv4().obtain_lease()
+ EphemeralDHCPv4(
+ MockDistro(),
+ ).obtain_lease()
assert len(m_dhcp.mock_calls) == 1
@@ -848,7 +899,9 @@ class TestEphemeralDhcpLeaseErrors:
def test_obtain_lease_umbrella_error(self, m_dhcp, error_class):
m_dhcp.side_effect = [error_class()]
with pytest.raises(NoDHCPLeaseError):
- EphemeralDHCPv4().obtain_lease()
+ EphemeralDHCPv4(
+ MockDistro(),
+ ).obtain_lease()
assert len(m_dhcp.mock_calls) == 1
@@ -857,7 +910,9 @@ class TestEphemeralDhcpLeaseErrors:
m_dhcp.side_effect = [error_class()]
with pytest.raises(error_class):
- with EphemeralDHCPv4():
+ with EphemeralDHCPv4(
+ MockDistro(),
+ ):
pass
assert len(m_dhcp.mock_calls) == 1
@@ -866,10 +921,9 @@ class TestEphemeralDhcpLeaseErrors:
def test_ctx_mgr_umbrella_error(self, m_dhcp, error_class):
m_dhcp.side_effect = [error_class()]
with pytest.raises(NoDHCPLeaseError):
- with EphemeralDHCPv4():
+ with EphemeralDHCPv4(
+ MockDistro(),
+ ):
pass
assert len(m_dhcp.mock_calls) == 1
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/net/test_ephemeral.py b/tests/unittests/net/test_ephemeral.py
index d2237faf..ddd9912c 100644
--- a/tests/unittests/net/test_ephemeral.py
+++ b/tests/unittests/net/test_ephemeral.py
@@ -5,6 +5,7 @@ from unittest import mock
import pytest
from cloudinit.net.ephemeral import EphemeralIPNetwork
+from tests.unittests.util import MockDistro
M_PATH = "cloudinit.net.ephemeral."
@@ -24,14 +25,17 @@ class TestEphemeralIPNetwork:
ipv6,
):
interface = object()
- with EphemeralIPNetwork(interface, ipv4=ipv4, ipv6=ipv6):
+ distro = MockDistro()
+ with EphemeralIPNetwork(distro, interface, ipv4=ipv4, ipv6=ipv6):
pass
expected_call_args_list = []
if ipv4:
expected_call_args_list.append(
mock.call(m_ephemeral_dhcp_v4.return_value)
)
- assert [mock.call(interface)] == m_ephemeral_dhcp_v4.call_args_list
+ assert [
+ mock.call(distro, interface)
+ ] == m_ephemeral_dhcp_v4.call_args_list
else:
assert [] == m_ephemeral_dhcp_v4.call_args_list
if ipv6:
diff --git a/tests/unittests/sources/test_azure.py b/tests/unittests/sources/test_azure.py
index a8588ea9..1c43aa66 100644
--- a/tests/unittests/sources/test_azure.py
+++ b/tests/unittests/sources/test_azure.py
@@ -3232,6 +3232,7 @@ class TestEphemeralNetworking:
assert mock_ephemeral_dhcp_v4.mock_calls == [
mock.call(
+ azure_ds.distro,
iface=iface,
dhcp_log_func=dsaz.dhcp_log_cb,
),
@@ -3258,6 +3259,7 @@ class TestEphemeralNetworking:
assert mock_ephemeral_dhcp_v4.mock_calls == [
mock.call(
+ azure_ds.distro,
iface=iface,
dhcp_log_func=dsaz.dhcp_log_cb,
),
@@ -3300,6 +3302,7 @@ class TestEphemeralNetworking:
assert mock_ephemeral_dhcp_v4.mock_calls == [
mock.call(
+ azure_ds.distro,
iface=None,
dhcp_log_func=dsaz.dhcp_log_cb,
),
@@ -3334,6 +3337,7 @@ class TestEphemeralNetworking:
assert mock_ephemeral_dhcp_v4.mock_calls == [
mock.call(
+ azure_ds.distro,
iface=None,
dhcp_log_func=dsaz.dhcp_log_cb,
),
@@ -3372,6 +3376,7 @@ class TestEphemeralNetworking:
mock_ephemeral_dhcp_v4.mock_calls
== [
mock.call(
+ azure_ds.distro,
iface=None,
dhcp_log_func=dsaz.dhcp_log_cb,
),
@@ -3539,6 +3544,7 @@ class TestProvisioning:
]
assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
)
@@ -3628,10 +3634,12 @@ class TestProvisioning:
]
assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
),
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
),
@@ -3747,10 +3755,12 @@ class TestProvisioning:
]
assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
),
mock.call(
+ self.azure_ds.distro,
"ethAttached1",
dsaz.dhcp_log_cb,
),
@@ -3904,10 +3914,12 @@ class TestProvisioning:
]
assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
),
mock.call(
+ self.azure_ds.distro,
"ethAttached1",
dsaz.dhcp_log_cb,
),
@@ -4008,6 +4020,7 @@ class TestProvisioning:
]
assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
),
@@ -4111,6 +4124,7 @@ class TestProvisioning:
]
assert self.mock_net_dhcp_maybe_perform_dhcp_discovery.mock_calls == [
mock.call(
+ self.azure_ds.distro,
None,
dsaz.dhcp_log_cb,
)
@@ -4356,6 +4370,3 @@ class TestValidateIMDSMetadata:
}
assert azure_ds.validate_imds_network_metadata(imds_md) is False
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/sources/test_cloudstack.py b/tests/unittests/sources/test_cloudstack.py
index b37400d3..463a9c7a 100644
--- a/tests/unittests/sources/test_cloudstack.py
+++ b/tests/unittests/sources/test_cloudstack.py
@@ -4,10 +4,8 @@ import os
import time
from cloudinit import helpers, util
-from cloudinit.sources.DataSourceCloudStack import (
- DataSourceCloudStack,
- get_latest_lease,
-)
+from cloudinit.net.dhcp import IscDhclient
+from cloudinit.sources.DataSourceCloudStack import DataSourceCloudStack
from tests.unittests.helpers import CiTestCase, ExitStack, mock
MOD_PATH = "cloudinit.sources.DataSourceCloudStack"
@@ -25,7 +23,10 @@ class TestCloudStackPasswordFetching(CiTestCase):
default_gw = "192.201.20.0"
get_latest_lease = mock.MagicMock(return_value=None)
self.patches.enter_context(
- mock.patch(mod_name + ".get_latest_lease", get_latest_lease)
+ mock.patch(
+ mod_name + ".dhcp.IscDhclient.get_latest_lease",
+ get_latest_lease,
+ )
)
get_default_gw = mock.MagicMock(return_value=default_gw)
@@ -151,7 +152,8 @@ class TestGetLatestLease(CiTestCase):
lease_d = self.tmp_dir()
self._populate_dir_list(lease_d, files)
self.assertEqual(
- self.tmp_path(expected, lease_d), get_latest_lease(lease_d)
+ self.tmp_path(expected, lease_d),
+ IscDhclient.get_latest_lease(lease_d),
)
def test_skips_dhcpv6_files(self):
@@ -198,13 +200,10 @@ class TestGetLatestLease(CiTestCase):
valid_2_path = self.tmp_path(valid_2, lease_d)
self._populate_dir_list(lease_d, [valid_1, valid_2])
- self.assertEqual(valid_2_path, get_latest_lease(lease_d))
+ self.assertEqual(valid_2_path, IscDhclient.get_latest_lease(lease_d))
# now update mtime on valid_2 to be older than valid_1 and re-check.
mtime = int(os.path.getmtime(valid_1_path)) - 1
os.utime(valid_2_path, (mtime, mtime))
- self.assertEqual(valid_1_path, get_latest_lease(lease_d))
-
-
-# vi: ts=4 expandtab
+ self.assertEqual(valid_1_path, IscDhclient.get_latest_lease(lease_d))
diff --git a/tests/unittests/sources/test_ec2.py b/tests/unittests/sources/test_ec2.py
index 2a311642..a7257668 100644
--- a/tests/unittests/sources/test_ec2.py
+++ b/tests/unittests/sources/test_ec2.py
@@ -879,7 +879,7 @@ class TestEc2(test_helpers.ResponsesTestCase):
ret = ds.get_data()
self.assertTrue(ret)
- m_dhcp.assert_called_once_with("eth9", None)
+ m_dhcp.assert_called_once_with(ds.distro, "eth9", None)
m_net4.assert_called_once_with(
broadcast="192.168.2.255",
interface="eth9",
@@ -1251,6 +1251,3 @@ class TesIdentifyPlatform(test_helpers.CiTestCase):
product_name="Not 3DS Outscale VM".lower(),
)
self.assertEqual(ec2.CloudNames.UNKNOWN, ec2.identify_platform())
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/sources/test_hetzner.py b/tests/unittests/sources/test_hetzner.py
index 6dbeb85b..024652c1 100644
--- a/tests/unittests/sources/test_hetzner.py
+++ b/tests/unittests/sources/test_hetzner.py
@@ -110,6 +110,7 @@ class TestDataSourceHetzner(CiTestCase):
self.assertTrue(ret)
m_net.assert_called_once_with(
+ ds.distro,
iface="eth0",
connectivity_url_data={
"url": "http://169.254.169.254/hetzner/v1/metadata/instance-id"
diff --git a/tests/unittests/sources/test_nwcs.py b/tests/unittests/sources/test_nwcs.py
index 052e322a..f96b585c 100644
--- a/tests/unittests/sources/test_nwcs.py
+++ b/tests/unittests/sources/test_nwcs.py
@@ -74,6 +74,7 @@ class TestDataSourceNWCS(CiTestCase):
self.assertTrue(ret)
m_net.assert_called_once_with(
+ ds.distro,
iface="eth0",
connectivity_url_data={
"url": "http://169.254.169.254/api/v1/metadata/instance-id"
diff --git a/tests/unittests/sources/test_openstack.py b/tests/unittests/sources/test_openstack.py
index b37a7570..6f588122 100644
--- a/tests/unittests/sources/test_openstack.py
+++ b/tests/unittests/sources/test_openstack.py
@@ -367,7 +367,7 @@ class TestOpenStackDataSource(test_helpers.ResponsesTestCase):
self.assertEqual(VENDOR_DATA, ds_os_local.vendordata_pure)
self.assertEqual(VENDOR_DATA2, ds_os_local.vendordata2_pure)
self.assertIsNone(ds_os_local.vendordata_raw)
- m_dhcp.assert_called_with("eth9", None)
+ m_dhcp.assert_called_with(distro, "eth9", None)
def test_bad_datasource_meta(self):
os_files = copy.deepcopy(OS_FILES)
diff --git a/tests/unittests/sources/test_oracle.py b/tests/unittests/sources/test_oracle.py
index c67cacef..0f0d9011 100644
--- a/tests/unittests/sources/test_oracle.py
+++ b/tests/unittests/sources/test_oracle.py
@@ -982,6 +982,7 @@ class TestNonIscsiRoot_GetDataBehaviour:
assert [
mock.call(
+ oracle_ds.distro,
iface=m_find_fallback_nic.return_value,
connectivity_url_data={
"headers": {"Authorization": "Bearer Oracle"},
@@ -1024,6 +1025,7 @@ class TestNonIscsiRoot_GetDataBehaviour:
assert [
mock.call(
+ oracle_ds.distro,
iface=m_find_fallback_nic.return_value,
connectivity_url_data={
"headers": {"Authorization": "Bearer Oracle"},
@@ -1209,6 +1211,3 @@ class TestNetworkConfig:
oracle_ds._network_config["config"]
), "Config not added"
assert "" == caplog.text
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/sources/test_upcloud.py b/tests/unittests/sources/test_upcloud.py
index 0bab508a..86c40845 100644
--- a/tests/unittests/sources/test_upcloud.py
+++ b/tests/unittests/sources/test_upcloud.py
@@ -242,7 +242,7 @@ class TestUpCloudNetworkSetup(CiTestCase):
self.assertTrue(ret)
self.assertTrue(m_dhcp.called)
- m_dhcp.assert_called_with("eth1", None)
+ m_dhcp.assert_called_with(ds.distro, "eth1", None)
m_net.assert_called_once_with(
broadcast="10.6.3.255",
@@ -333,6 +333,3 @@ class TestUpCloudDatasourceLoading(CiTestCase):
["cloudinit.sources"],
)
self.assertEqual([DataSourceUpCloud], found)
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/sources/test_vultr.py b/tests/unittests/sources/test_vultr.py
index 488df4f3..ba21ae24 100644
--- a/tests/unittests/sources/test_vultr.py
+++ b/tests/unittests/sources/test_vultr.py
@@ -398,7 +398,7 @@ class TestDataSourceVultr(CiTestCase):
# Override ephemeral for proper unit testing
def ephemeral_init(
- self, iface="", connectivity_url_data=None, tmp_dir=None
+ self, distro, iface="", connectivity_url_data=None, tmp_dir=None
):
global FINAL_INTERFACE_USED
FINAL_INTERFACE_USED = iface
@@ -492,6 +492,3 @@ class TestDataSourceVultr(CiTestCase):
pass
self.assertEqual(FINAL_INTERFACE_USED, INTERFACES[3])
-
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/util.py b/tests/unittests/util.py
index e7094ec5..8c6682e1 100644
--- a/tests/unittests/util.py
+++ b/tests/unittests/util.py
@@ -73,7 +73,8 @@ class MockDistro(distros.Distro):
def set_hostname(self, hostname, fqdn=None):
pass
- def uses_systemd(self):
+ @staticmethod
+ def uses_systemd():
return True
def get_primary_arch(self):