Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor: remove dependency on netifaces #4634

Merged
merged 7 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 80 additions & 127 deletions cloudinit/sources/DataSourceVMware.py
catmsred marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,6 @@
* EnvVars
* GuestInfo
* IMC (Guest Customization)

Netifaces (https://github.com/al45tair/netifaces)

Please note this module relies on the netifaces project to introspect the
runtime, network configuration of the host on which this datasource is
running. This is in contrast to the rest of cloud-init which uses the
cloudinit/netinfo module.

The reasons for using netifaces include:

* Netifaces is built in C and is more portable across multiple systems
and more deterministic than shell exec'ing local network commands and
parsing their output.

* Netifaces provides a stable way to determine the view of the host's
network after DHCP has brought the network online. Unlike most other
datasources, this datasource still provides support for JINJA queries
based on networking information even when the network is based on a
DHCP lease. While this does not tie this datasource directly to
netifaces, it does mean the ability to consistently obtain the
correct information is paramount.

* It is currently possible to execute this datasource on macOS
(which many developers use today) to print the output of the
get_host_info function. This function calls netifaces to obtain
the same runtime network configuration that the datasource would
persist to the local system's instance data.

However, the netinfo module fails on macOS. The result is either a
hung operation that requires a SIGINT to return control to the user,
or, if brew is used to install iproute2mac, the ip commands are used
but produce output the netinfo module is unable to parse.

While macOS is not a target of cloud-init, this feature is quite
useful when working on this datasource.

For more information about this behavior, please see the following
PR comment, https://bit.ly/3fG7OVh.

The authors of this datasource are not opposed to moving away from
netifaces. The goal may be to eventually do just that. This proviso was
added to the top of this module as a way to remind future-us and others
why netifaces was used in the first place in order to either smooth the
transition away from netifaces or embrace it further up the cloud-init
stack.
"""

import collections
Expand All @@ -72,9 +27,7 @@
import socket
import time

import netifaces

from cloudinit import atomic_helper, dmi, log, net, sources, util
from cloudinit import atomic_helper, dmi, log, net, netinfo, sources, util
from cloudinit.sources.helpers.vmware.imc import guestcust_util
from cloudinit.subp import ProcessExecutionError, subp, which

Expand Down Expand Up @@ -776,91 +729,64 @@ def get_default_ip_addrs():
addresses associated with the device used by the default route for a given
address.
"""
# TODO(promote and use netifaces in cloudinit.net* modules)
catmsred marked this conversation as resolved.
Show resolved Hide resolved
gateways = netifaces.gateways()
if "default" not in gateways:
return None, None

default_gw = gateways["default"]
if (
netifaces.AF_INET not in default_gw
and netifaces.AF_INET6 not in default_gw
):
return None, None

# Get ipv4 and ipv6 interfaces associated with default routes
ipv4_if = None
ipv6_if = None
routes = netinfo.route_info()
for route in routes["ipv4"]:
if route["destination"] == "0.0.0.0":
ipv4_if = route["iface"]
catmsred marked this conversation as resolved.
Show resolved Hide resolved
break
for route in routes["ipv6"]:
if route["destination"] == "::/0":
ipv6_if = route["iface"]
catmsred marked this conversation as resolved.
Show resolved Hide resolved
break

# Get ip address associated with default interface
ipv4 = None
ipv6 = None

gw4 = default_gw.get(netifaces.AF_INET)
if gw4:
_, dev4 = gw4
addr4_fams = netifaces.ifaddresses(dev4)
if addr4_fams:
af_inet4 = addr4_fams.get(netifaces.AF_INET)
if af_inet4:
if len(af_inet4) > 1:
LOG.debug(
"device %s has more than one ipv4 address: %s",
dev4,
af_inet4,
)
elif "addr" in af_inet4[0]:
ipv4 = af_inet4[0]["addr"]

# Try to get the default IPv6 address by first seeing if there is a default
# IPv6 route.
gw6 = default_gw.get(netifaces.AF_INET6)
if gw6:
_, dev6 = gw6
addr6_fams = netifaces.ifaddresses(dev6)
if addr6_fams:
af_inet6 = addr6_fams.get(netifaces.AF_INET6)
if af_inet6:
if len(af_inet6) > 1:
LOG.debug(
"device %s has more than one ipv6 address: %s",
dev6,
af_inet6,
)
elif "addr" in af_inet6[0]:
ipv6 = af_inet6[0]["addr"]
netdev = netinfo.netdev_info()
if ipv4_if in netdev:
addrs = netdev[ipv4_if]["ipv4"]
if len(addrs) > 1:
LOG.debug(
"device %s has more than one ipv4 address: %s", ipv4_if, addrs
)
elif len(addrs) == 1 and "ip" in addrs[0]:
ipv4 = addrs[0]["ip"]
if ipv6_if in netdev:
addrs = netdev[ipv6_if]["ipv6"]
if len(addrs) > 1:
LOG.debug(
"device %s has more than one ipv6 address: %s", ipv6_if, addrs
)
elif len(addrs) == 1 and "ip" in addrs[0]:
ipv6 = addrs[0]["ip"]

# If there is a default IPv4 address but not IPv6, then see if there is a
# single IPv6 address associated with the same device associated with the
# default IPv4 address.
if ipv4 and not ipv6:
af_inet6 = addr4_fams.get(netifaces.AF_INET6)
if af_inet6:
if len(af_inet6) > 1:
LOG.debug(
"device %s has more than one ipv6 address: %s",
dev4,
af_inet6,
)
elif "addr" in af_inet6[0]:
ipv6 = af_inet6[0]["addr"]
if ipv4 is not None and ipv6 is None:
for dev_name in netdev:
for addr in netdev[dev_name]["ipv4"]:
if addr["ip"] == ipv4 and len(netdev[dev_name]["ipv6"]) == 1:
ipv6 = netdev[dev_name]["ipv6"][0]["ip"]
break

# If there is a default IPv6 address but not IPv4, then see if there is a
# single IPv4 address associated with the same device associated with the
# default IPv6 address.
if not ipv4 and ipv6:
af_inet4 = addr6_fams.get(netifaces.AF_INET)
if af_inet4:
if len(af_inet4) > 1:
LOG.debug(
"device %s has more than one ipv4 address: %s",
dev6,
af_inet4,
)
elif "addr" in af_inet4[0]:
ipv4 = af_inet4[0]["addr"]
if ipv4 is None and ipv6 is not None:
for dev_name in netdev:
for addr in netdev[dev_name]["ipv6"]:
if addr["ip"] == ipv6 and len(netdev[dev_name]["ipv4"]) == 1:
ipv4 = netdev[dev_name]["ipv4"][0]["ip"]
break

return ipv4, ipv6


# patched socket.getfqdn() - see https://bugs.python.org/issue5004


def getfqdn(name=""):
"""Get fully qualified domain name from name.
An empty argument is interpreted as meaning the local host.
Expand Down Expand Up @@ -895,6 +821,33 @@ def is_valid_ip_addr(val):
)


def convert_to_netifaces_format(addr):
"""
Takes a cloudinit.netinfo formatted address and converts to netifaces
format, since this module was originally written with netifaces as the
network introspection module.
netifaces format:
{
"broadcast": "10.15.255.255",
"netmask": "255.240.0.0",
"addr": "10.0.1.4"
}

cloudinit.netinfo format:
{
"ip": "10.0.1.4",
"mask": "255.240.0.0",
"bcast": "10.15.255.255",
"scope": "global",
}
"""
return {
"broadcast": addr["bcast"],
"netmask": addr["mask"],
"addr": addr["ip"],
}


def get_host_info():
"""
Returns host information such as the host name and network interfaces.
Expand Down Expand Up @@ -925,16 +878,16 @@ def get_host_info():
by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"]
by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"]

ifaces = netifaces.interfaces()
ifaces = netinfo.netdev_info()
for dev_name in ifaces:
addr_fams = netifaces.ifaddresses(dev_name)
af_link = addr_fams.get(netifaces.AF_LINK)
af_inet4 = addr_fams.get(netifaces.AF_INET)
af_inet6 = addr_fams.get(netifaces.AF_INET6)

mac = None
if af_link and "addr" in af_link[0]:
mac = af_link[0]["addr"]
af_inet4 = []
af_inet6 = []
for addr in ifaces[dev_name]["ipv4"]:
af_inet4.append(convert_to_netifaces_format(addr))
for addr in ifaces[dev_name]["ipv6"]:
af_inet6.append(convert_to_netifaces_format(addr))

mac = ifaces[dev_name].get("hwaddr")

# Do not bother recording localhost
if mac == "00:00:00:00:00:00":
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ module = [
"debconf",
"httplib",
"jsonpatch",
"netifaces",
"paramiko.*",
"pip.*",
"pycloudlib.*",
Expand Down
9 changes: 0 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,3 @@ jsonpatch

# For validating cloud-config sections per schema definitions
jsonschema

# Used by DataSourceVMware to inspect the host's network configuration during
# the "setup()" function.
#
# This allows a host that uses DHCP to bring up the network during BootLocal
# and still participate in instance-data by gathering the network in detail at
# runtime and merge that information into the metadata and repersist that to
# disk.
netifaces>=0.10.4
Loading
Loading