summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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):