diff options
Diffstat (limited to 'cloudinit/net/dhcp.py')
-rw-r--r-- | cloudinit/net/dhcp.py | 208 |
1 files changed, 126 insertions, 82 deletions
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py index 4394c68b..f9af18cf 100644 --- a/cloudinit/net/dhcp.py +++ b/cloudinit/net/dhcp.py @@ -4,25 +4,28 @@ # # This file is part of cloud-init. See LICENSE file for license information. -import configobj import logging import os import re import signal import time from io import StringIO +from typing import Any, Dict + +import configobj +from cloudinit import subp, temp_utils, util from cloudinit.net import ( - EphemeralIPv4Network, find_fallback_nic, get_devicelist, - has_url_connectivity) + EphemeralIPv4Network, + find_fallback_nic, + get_devicelist, + has_url_connectivity, +) from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip -from cloudinit import temp_utils -from cloudinit import subp -from cloudinit import util LOG = logging.getLogger(__name__) -NETWORKD_LEASES_DIR = '/run/systemd/netif/leases' +NETWORKD_LEASES_DIR = "/run/systemd/netif/leases" class InvalidDHCPLeaseFileError(Exception): @@ -38,21 +41,28 @@ class NoDHCPLeaseError(Exception): class EphemeralDHCPv4(object): - def __init__(self, iface=None, connectivity_url=None, dhcp_log_func=None): + def __init__( + self, + iface=None, + connectivity_url_data: Dict[str, Any] = None, + dhcp_log_func=None, + ): self.iface = iface self._ephipv4 = None self.lease = None self.dhcp_log_func = dhcp_log_func - self.connectivity_url = connectivity_url + self.connectivity_url_data = connectivity_url_data def __enter__(self): """Setup sandboxed dhcp context, unless connectivity_url can already be reached.""" - if self.connectivity_url: - if has_url_connectivity(self.connectivity_url): + if self.connectivity_url_data: + if has_url_connectivity(self.connectivity_url_data): LOG.debug( - 'Skip ephemeral DHCP setup, instance has connectivity' - ' to %s', self.connectivity_url) + "Skip ephemeral DHCP setup, instance has connectivity" + " to %s", + self.connectivity_url_data, + ) return return self.obtain_lease() @@ -81,31 +91,39 @@ class EphemeralDHCPv4(object): return self.lease try: leases = maybe_perform_dhcp_discovery( - self.iface, self.dhcp_log_func) + self.iface, self.dhcp_log_func + ) except InvalidDHCPLeaseFileError as e: raise NoDHCPLeaseError() from e if not leases: raise NoDHCPLeaseError() self.lease = leases[-1] - LOG.debug("Received dhcp lease on %s for %s/%s", - self.lease['interface'], self.lease['fixed-address'], - self.lease['subnet-mask']) - nmap = {'interface': 'interface', 'ip': 'fixed-address', - 'prefix_or_mask': 'subnet-mask', - 'broadcast': 'broadcast-address', - 'static_routes': [ - 'rfc3442-classless-static-routes', - 'classless-static-routes' - ], - 'router': 'routers'} + LOG.debug( + "Received dhcp lease on %s for %s/%s", + self.lease["interface"], + self.lease["fixed-address"], + self.lease["subnet-mask"], + ) + nmap = { + "interface": "interface", + "ip": "fixed-address", + "prefix_or_mask": "subnet-mask", + "broadcast": "broadcast-address", + "static_routes": [ + "rfc3442-classless-static-routes", + "classless-static-routes", + ], + "router": "routers", + } kwargs = self.extract_dhcp_options_mapping(nmap) - if not kwargs['broadcast']: - kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip']) - if kwargs['static_routes']: - kwargs['static_routes'] = ( - parse_static_routes(kwargs['static_routes'])) - if self.connectivity_url: - kwargs['connectivity_url'] = self.connectivity_url + if not kwargs["broadcast"]: + kwargs["broadcast"] = bcip(kwargs["prefix_or_mask"], kwargs["ip"]) + if kwargs["static_routes"]: + kwargs["static_routes"] = parse_static_routes( + kwargs["static_routes"] + ) + if self.connectivity_url_data: + kwargs["connectivity_url_data"] = self.connectivity_url_data ephipv4 = EphemeralIPv4Network(**kwargs) ephipv4.__enter__() self._ephipv4 = ephipv4 @@ -116,16 +134,15 @@ class EphemeralDHCPv4(object): for internal_reference, lease_option_names in nmap.items(): if isinstance(lease_option_names, list): self.get_first_option_value( - internal_reference, - lease_option_names, - result + internal_reference, lease_option_names, result ) else: result[internal_reference] = self.lease.get(lease_option_names) return result - def get_first_option_value(self, internal_mapping, - lease_option_names, result): + def get_first_option_value( + self, internal_mapping, lease_option_names, result + ): for different_names in lease_option_names: if not result.get(internal_mapping): result[internal_mapping] = self.lease.get(different_names) @@ -147,19 +164,20 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None): if nic is None: nic = find_fallback_nic() if nic is None: - LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.') + LOG.debug("Skip dhcp_discovery: Unable to find fallback nic.") return [] elif nic not in get_devicelist(): LOG.debug( - 'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic) + "Skip dhcp_discovery: nic %s not found in get_devicelist.", nic + ) return [] - dhclient_path = subp.which('dhclient') + dhclient_path = subp.which("dhclient") if not dhclient_path: - LOG.debug('Skip dhclient configuration: No dhclient command found.') + LOG.debug("Skip dhclient configuration: No dhclient command found.") return [] - with temp_utils.tempdir(rmtree_ignore_errors=True, - prefix='cloud-init-dhcp-', - needs_exe=True) as tdir: + with temp_utils.tempdir( + rmtree_ignore_errors=True, prefix="cloud-init-dhcp-", needs_exe=True + ) as tdir: # Use /var/tmp because /run/cloud-init/tmp is mounted noexec return dhcp_discovery(dhclient_path, nic, tdir, dhcp_log_func) @@ -173,25 +191,28 @@ def parse_dhcp_lease_file(lease_file): @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile content. """ - lease_regex = re.compile(r"lease {(?P<lease>[^}]*)}\n") + 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)) + "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(';'): + for line in lease.split(";"): # Strip newlines, double-quotes and option prefix - line = line.strip().replace('"', '').replace('option ', '') + line = line.strip().replace('"', "").replace("option ", "") if not line: continue - lease_options.append(line.split(' ', 1)) + 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)) + "Cannot parse dhcp lease file {0}. No leases found".format( + lease_file + ) + ) return dhcp_leases @@ -208,17 +229,17 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): @return: A list of dicts of representing the dhcp leases parsed from the dhcp.leases file or empty list. """ - LOG.debug('Performing a dhcp discovery on %s', interface) + LOG.debug("Performing a dhcp discovery on %s", interface) # XXX We copy dhclient out of /sbin/dhclient to avoid dealing with strict # app armor profiles which disallow running dhclient -sf <our-script-file>. # 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. - sandbox_dhclient_cmd = os.path.join(cleandir, 'dhclient') + sandbox_dhclient_cmd = os.path.join(cleandir, "dhclient") util.copy(dhclient_cmd_path, sandbox_dhclient_cmd) - pid_file = os.path.join(cleandir, 'dhclient.pid') - lease_file = os.path.join(cleandir, 'dhcp.leases') + pid_file = os.path.join(cleandir, "dhclient.pid") + lease_file = os.path.join(cleandir, "dhcp.leases") # In some cases files in /var/tmp may not be executable, launching dhclient # from there will certainly raise 'Permission denied' error. Try launching @@ -230,9 +251,19 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): # 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) - cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, - '-pf', pid_file, interface, '-sf', '/bin/true'] + subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True) + cmd = [ + sandbox_dhclient_cmd, + "-1", + "-v", + "-lf", + lease_file, + "-pf", + pid_file, + interface, + "-sf", + "/bin/true", + ] out, err = subp.subp(cmd, capture=True) # Wait for pid file and lease file to appear, and for the process @@ -243,13 +274,16 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): # 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) + [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)) + LOG.warning( + "dhclient did not produce expected files: %s", + ", ".join(os.path.basename(f) for f in missing), + ) return [] - ppid = 'unknown' + ppid = "unknown" daemonized = False for _ in range(0, 1000): pid_content = util.load_file(pid_file).strip() @@ -260,7 +294,7 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): else: ppid = util.get_proc_ppid(pid) if ppid == 1: - LOG.debug('killing dhclient with pid=%s', pid) + LOG.debug("killing dhclient with pid=%s", pid) os.kill(pid, signal.SIGKILL) daemonized = True break @@ -268,8 +302,11 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): if not daemonized: LOG.error( - 'dhclient(pid=%s, parentpid=%s) failed to daemonize after %s ' - 'seconds', pid_content, ppid, 0.01 * 1000 + "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) @@ -301,7 +338,8 @@ def networkd_load_leases(leases_d=None): return ret for lfile in os.listdir(leases_d): ret[lfile] = networkd_parse_lease( - util.load_file(os.path.join(leases_d, lfile))) + util.load_file(os.path.join(leases_d, lfile)) + ) return ret @@ -316,7 +354,7 @@ def networkd_get_option_from_leases(keyname, leases_d=None): def parse_static_routes(rfc3442): - """ parse rfc3442 format and return a list containing tuple of strings. + """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, @@ -346,10 +384,12 @@ def parse_static_routes(rfc3442): 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)) + 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) current_idx = 0 @@ -362,32 +402,32 @@ def parse_static_routes(rfc3442): 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]) + 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]) + 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]) + 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]) + 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 @@ -395,15 +435,19 @@ def parse_static_routes(rfc3442): _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]) + 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) + LOG.error( + 'Parsed invalid net length "%s". Verify DHCP ' + "rfc3442-classless-static-routes value.", + net_length, + ) return static_routes static_routes.append(("%s/%s" % (net_address, net_length), gateway)) return static_routes + # vi: ts=4 expandtab |