← Back to team overview

cloud-init-dev team mailing list archive

Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master

 


Diff comments:

> diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
> index 3642fb1..c5e78a2 100644
> --- a/cloudinit/net/__init__.py
> +++ b/cloudinit/net/__init__.py
> @@ -14,171 +14,62 @@ from cloudinit.net.network_state import mask_to_net_prefix
>  from cloudinit import util
>  from cloudinit.url_helper import UrlError, readurl
>  
> -LOG = logging.getLogger(__name__)
> -SYS_CLASS_NET = "/sys/class/net/"
> -DEFAULT_PRIMARY_INTERFACE = 'eth0'
> -
> -
> -def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
> -    """Sorting for Humans: natural sort order. Can be use as the key to sort
> -    functions.
> -    This will sort ['eth0', 'ens3', 'ens10', 'ens12', 'ens8', 'ens0'] as
> -    ['ens0', 'ens3', 'ens8', 'ens10', 'ens12', 'eth0'] instead of the simple
> -    python way which will produce ['ens0', 'ens10', 'ens12', 'ens3', 'ens8',
> -    'eth0']."""
> -    return [int(text) if text.isdigit() else text.lower()
> -            for text in re.split(_nsre, s)]
> -
> -
> -def get_sys_class_path():
> -    """Simple function to return the global SYS_CLASS_NET."""
> -    return SYS_CLASS_NET
> -
> -
> -def sys_dev_path(devname, path=""):
> -    return get_sys_class_path() + devname + "/" + path
> -
> -
> -def read_sys_net(devname, path, translate=None,
> -                 on_enoent=None, on_keyerror=None,
> -                 on_einval=None):
> -    dev_path = sys_dev_path(devname, path)
> -    try:
> -        contents = util.load_file(dev_path)
> -    except (OSError, IOError) as e:
> -        e_errno = getattr(e, 'errno', None)
> -        if e_errno in (errno.ENOENT, errno.ENOTDIR):
> -            if on_enoent is not None:
> -                return on_enoent(e)
> -        if e_errno in (errno.EINVAL,):
> -            if on_einval is not None:
> -                return on_einval(e)
> -        raise
> -    contents = contents.strip()
> -    if translate is None:
> -        return contents
> -    try:
> -        return translate[contents]
> -    except KeyError as e:
> -        if on_keyerror is not None:
> -            return on_keyerror(e)
> -        else:
> -            LOG.debug("Found unexpected (not translatable) value"
> -                      " '%s' in '%s", contents, dev_path)
> -            raise
> -
> -
> -def read_sys_net_safe(iface, field, translate=None):
> -    def on_excp_false(e):
> -        return False
> -    return read_sys_net(iface, field,
> -                        on_keyerror=on_excp_false,
> -                        on_enoent=on_excp_false,
> -                        on_einval=on_excp_false,
> -                        translate=translate)
> +if util.is_FreeBSD():
> +    from cloudinit.net import freebsd as netimpl
> +else:
> +    from cloudinit.net import linux as netimpl
>  
>  
> -def read_sys_net_int(iface, field):
> -    val = read_sys_net_safe(iface, field)
> -    if val is False:
> -        return None
> -    try:
> -        return int(val)
> -    except ValueError:
> -        return None
> +LOG = logging.getLogger(__name__)
> +LO_DEVS = ['lo', 'lo0']
>  
>  
>  def is_up(devname):
> -    # The linux kernel says to consider devices in 'unknown'
> -    # operstate as up for the purposes of network configuration. See
> -    # Documentation/networking/operstates.txt in the kernel source.
> -    translate = {'up': True, 'unknown': True, 'down': False}
> -    return read_sys_net_safe(devname, "operstate", translate=translate)
> +    return netimpl.is_up(devname)
>  
>  
>  def is_wireless(devname):
> -    return os.path.exists(sys_dev_path(devname, "wireless"))
> +    return netimpl.is_wireless(devname)
>  
>  
>  def is_bridge(devname):
> -    return os.path.exists(sys_dev_path(devname, "bridge"))
> +    return netimpl.is_wireless(devname)
>  
>  
>  def is_bond(devname):
> -    return os.path.exists(sys_dev_path(devname, "bonding"))
> +    return netimpl.is_bond(devname)
>  
>  
>  def is_renamed(devname):
> -    """

In your rework, you've pulled a lot of function comments/docstrings directly into cloudinit/net/linux. Generally what I'd like to see in cloud-init is a top-level docstring per function describing it's intent, even if it is only a one-liner
"""Return True if a network device has been renamed."""

> -    /* interface name assignment types (sysfs name_assign_type attribute) */
> -    #define NET_NAME_UNKNOWN	0	/* unknown origin (not exposed to user) */
> -    #define NET_NAME_ENUM		1	/* enumerated by kernel */
> -    #define NET_NAME_PREDICTABLE	2	/* predictably named by the kernel */
> -    #define NET_NAME_USER		3	/* provided by user-space */
> -    #define NET_NAME_RENAMED	4	/* renamed by user-space */
> -    """
> -    name_assign_type = read_sys_net_safe(devname, 'name_assign_type')
> -    if name_assign_type and name_assign_type in ['3', '4']:
> -        return True
> -    return False
> +    return netimpl.is_renamed(devname)
>  
>  
>  def is_vlan(devname):
> -    uevent = str(read_sys_net_safe(devname, "uevent"))
> -    return 'DEVTYPE=vlan' in uevent.splitlines()
> +    return netimpl.is_renamed(devname)

Typo, is_vlan instead of is_renamed.

>  
>  
>  def is_connected(devname):
> -    # is_connected isn't really as simple as that.  2 is
> -    # 'physically connected'. 3 is 'not connected'. but a wlan interface will
> -    # always show 3.
> -    iflink = read_sys_net_safe(devname, "iflink")
> -    if iflink == "2":
> -        return True
> -    if not is_wireless(devname):
> -        return False
> -    LOG.debug("'%s' is wireless, basing 'connected' on carrier", devname)
> -    return read_sys_net_safe(devname, "carrier",
> -                             translate={'0': False, '1': True})
> +    return netimpl.is_connected(devname)
>  
>  
>  def is_physical(devname):
> -    return os.path.exists(sys_dev_path(devname, "device"))
> +    return netimpl.is_physical(devname)
>  
>  
>  def is_present(devname):
> -    return os.path.exists(sys_dev_path(devname))
> +    return netimpl.is_present(devname)
>  
>  
>  def device_driver(devname):
> -    """Return the device driver for net device named 'devname'."""

Here is an example of a simple docstring I'd like to see left in the top-level module if we can.

> -    driver = None
> -    driver_path = sys_dev_path(devname, "device/driver")
> -    # driver is a symlink to the driver *dir*
> -    if os.path.islink(driver_path):
> -        driver = os.path.basename(os.readlink(driver_path))
> -
> -    return driver
> +    return netimpl.device_driver(devname)
>  
>  
>  def device_devid(devname):
> -    """Return the device id string for net device named 'devname'."""
> -    dev_id = read_sys_net_safe(devname, "device/device")
> -    if dev_id is False:
> -        return None
> -
> -    return dev_id
> +    return netimpl.device_devid(devname)
>  
>  
>  def get_devicelist():
> -    try:
> -        devs = os.listdir(get_sys_class_path())
> -    except OSError as e:
> -        if e.errno == errno.ENOENT:
> -            devs = []
> -        else:
> -            raise
> -    return devs
> +    return netimpl.get_devicelist()
>  
>  
>  class ParserError(Exception):
> diff --git a/cloudinit/net/common.py b/cloudinit/net/common.py
> new file mode 100644
> index 0000000..579739c
> --- /dev/null
> +++ b/cloudinit/net/common.py
> @@ -0,0 +1,40 @@
> +# Copyright (C) 2018 Canonical Ltd.
> +#
> +# This file is part of cloud-init. See LICENSE file for license information.
> +"""
> +This module is used for all high-level functions common to linux & freebsd,
> +that are also used in linux & freebsd!
> +They cannot be put them into net/__init__ because otherwise we'd end up with
> +cyclic imports.
> +"""
> +
> +from . import LO_DEVS, get_devicelist, interface_has_own_mac, is_bridge, is_vlan, get_interface_mac, device_driver, device_devid
> +
> +
> +def get_interfaces():

In this case I may be okay with the separate module given that we now have decoupled distro-speicifc modules. There are a couple ways we can avoid this circular import. I'll look a bit more into this tomorrow.

> +    """Return list of interface tuples (name, mac, driver, device_id)
> +
> +    Bridges and any devices that have a 'stolen' mac are excluded."""
> +    ret = []
> +    devs = get_devicelist()
> +    # 16 somewhat arbitrarily chosen.  Normally a mac is 6 '00:' tokens.
> +    zero_mac = ':'.join(('00', ) * 16)
> +    for name in devs:
> +        if not interface_has_own_mac(name):
> +            continue
> +        if is_bridge(name):
> +            continue
> +        if is_vlan(name):
> +            continue
> +        mac = get_interface_mac(name)
> +        # some devices may not have a mac (tun0)
> +        if not mac:
> +            continue
> +        # skip nics that have no mac (00:00....)
> +        if name not in LO_DEVS and mac == zero_mac[:len(mac)]:
> +            continue
> +        ret.append((name, mac, device_driver(name), device_devid(name)))
> +    return ret
> +
> +
> +# vi: ts=4 expandtab
> diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
> new file mode 100644
> index 0000000..4f2179b
> --- /dev/null
> +++ b/cloudinit/net/freebsd.py
> @@ -0,0 +1,268 @@
> +# Copyright (C) 2018 Canonical Ltd.

Feel free to ditch to top two lines. For new files the follow header comment will suffice

# This file is part of cloud-init. See LICENSE file for license information.

> +#
> +# This file is part of cloud-init. See LICENSE file for license information.
> +

Add a docstring to this module and cloudinit.net.linux something like
# FreeBSD-specific network utility functions

> +import errno
> +import logging
> +import re
> +
> +from cloudinit.net.network_state import mask_to_net_prefix
> +from cloudinit import util
> +
> +LOG = logging.getLogger(__name__)
> +DEFAULT_PRIMARY_INTERFACE = 'vtnet0'
> +
> +
> +def _ifconfig(*args):
> +    return util.subp(['ifconfig', *args])
> +
> +
> +def _sysctl(*args):
> +    return util.subp(['sysctl', *args])
> +
> +
> +def _pciconf(*args):
> +    return util.subp(['pciconf', *args])
> +
> +
> +def _route(*args):
> +    return util.subp(['route', *args])
> +
> +
> +def _parse_devname(devname):
> +    dev = re.match("^(?P<driver>[a-z]+)(?P<devid>[0-9]+)$", devname)
> +    if dev is None:
> +        return None
> +    return (
> +        dev.group("driver"),
> +        dev.group("devid"),
> +    )
> +
> +
> +def is_up(devname):
> +    output = _ifconfig(['-u', devname])

Since util.subp returns a tuple, prevailing style in cloud-init suggests we do something like the following so our future selves understand the intent of the elements returned.

out, err = _ifconfig(['-u', devname])
if out is '' or err is not '':
  return False.

Same comment throughout for _sysctl returns and _pcicconf

Alternatively, function calls cost more than inline code at the callsites. Since the function calss _ifconfig, _sysctl, _pciconf and _route are only short one-liners, maybe is makes sense to just drop those functions as they are only thin wrappers that further obfuscate what is being called.

Then is_up (and it's ilk) could be:

def is_up(devname):
    out, err = util.subp(['ifconfig', '-u', devname])
    if out == '' or err != '':
       return False

> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    return devname in output[0].split("\n")[0]
> +
> +
> +def is_wireless(devname):
> +    output = _sysctl('-n', "net.wlan.devices")
> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    wlan_devs = output[0].strip().split(" ")
> +    return devname in wlan_devs
> +
> +
> +def is_bridge(devname):
> +    output = _ifconfig(['-g', 'bridge'])
> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    bridges = output[0].strip().split("\n")
> +    return devname in bridges
> +
> +
> +def is_bond(devname):
> +    output = _ifconfig(['-g', 'lagg'])
> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    bonds = output[0].strip().split("\n")
> +    return devname in bonds
> +
> +
> +def is_vlan(devname):
> +    output = _ifconfig(['-g', 'vlan'])
> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    vlans = output[0].strip().split("\n")
> +    return devname in vlans
> +
> +
> +def is_renamed(devname):
> +    # we can only tell if a *physical* device was renamed, so
> +    if not is_physical(devname):
> +        return False
> +
> +    dev = _parse_devname(devname)
> +    renamed = _syctl(['-n', "dev.", dev[0] + "." + dev[1] + ".%driver"])
> +    if renamed[0] is '' or renamed[1] is not '':

Still looking into the duplication for my next review pass.

> +        return False
> +
> +    return dev[0] == renamed[0]
> +
> +
> +def is_connected(devname):
> +    output = _ifconfig([devname])
> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    ifcfg = output[0].split("\n")
> +    status_re = re.compile("^\s+status: active$")
> +    for l in ifcfg:
> +        if status_re.match(l):
> +            return True
> +    return False
> +
> +
> +def is_physical(devname):
> +    # reject interfaces like: vnet0:3
> +    if ":" in devname:
> +        return False
> +
> +    dev = _parse_devname(devname)
> +    if dev is None:
> +        return False
> +
> +    output = _sysctl("dev." + dev[0] + "." + dev[1])
> +    if output[0] is '' or output[1] is not '':
> +        return False
> +
> +    return True
> +
> +
> +def is_present(devname):
> +    return devname in get_devicelist()
> +
> +
> +def device_driver(devname):
> +    dev = _parse_devname(devname)
> +    driver = _syctl(['-n', "dev.", dev[0] + "." + dev[1] + ".%driver"])
> +    if driver[0] is '' or output[1] is not '':
> +        return None
> +
> +    return driver[0]
> +
> +
> +def device_devid(devname):
> +    if not is_physical(devname):
> +        return None
> +
> +    dev = _parse_devname(devname)
> +    output = _sysctl(['-n', "dev." + dev[0] + "." + dev[1] + ".%parent"])
> +    if output[0] is '' or output[1] is not '':
> +        return None
> +
> +    # well, isn't that clear? rather than parsing `pciconf -l` output, we just
> +    # *read* (-r) a *halfword* (-h) 44 bytes away from the %parent
> +    devid = _pciconf(['-rh', output[0], '44'])
> +    if devid[0] is '' or devid[1] is not '':
> +        return None
> +    return "0x" + devid[0]
> +
> +
> +def get_devicelist():
> +    maybe_devs = _ifconfig(['-l']).strip().split(' ')
> +    if maybe_devs == ['']:
> +        return []
> +    return maybe_devs
> +
> +
> +def find_fallback_nic(blacklist_drivers=None):
> +    output = _route(["-n", "show", "default"])
> +    if output[0] is '' or output[1] is not '':
> +        return None
> +
> +    route = output[0].split("\n")
> +    via_re = re.compile("^\s+interface: (?P<netif>.+)$")
> +    for l in route:
> +        maybe_netif = via_re.match(l)
> +        if maybe_netif is None:
> +            continue
> +
> +        driver = device_driver(maybe_netif)
> +        if driver not in blacklist_drivers:
> +            return maybe_netif.group("netif")
> +
> +    return None
> +
> +
> +def interface_has_own_mac(ifname, strict=False):
> +    # breakdown:
> +    # physical interfaces have their own MAC
> +    # bridges have their own MAC, unless net.link.bridge.inherit_mac is non-zero
> +    # vlan interfaces inherit MACs
> +    # bond interfaces have ??
> +    # tunnel interfaces have ??
> +    # virtual interfaces *usually* have their own, unless set not to
> +    if is_physical(ifname):
> +        return True
> +
> +    if is_bridge(ifname):
> +        output = _sysctl(['-n', 'net.link.bridge.inherit_mac'])
> +        inherit_mac = output[0].strip()
> +        if inherit_mac == '0':
> +            return False
> +        return True
> +    # for now:
> +    return False
> +
> +
> +def _get_current_rename_info(check_downable=True):
> +    """Collect information necessary for rename_interfaces.
> +
> +    returns a dictionary by mac address like:
> +       {name:
> +         {
> +          'downable': None or boolean indicating that the
> +                      device has only automatically assigned ip addrs.
> +          'device_id': Device id value (if it has one)
> +          'driver': Device driver (if it has one)
> +          'mac': mac address (in lower case)
> +          'name': name
> +          'up': boolean: is_up(name)
> +         }}
> +    """
> +    raise NotImplemented("TODO")
> +
> +
> +def _rename_interfaces(renames,
> +                       strict_present=True,
> +                       strict_busy=True,
> +                       current_info=None):
> +
> +    raise NotImplemented("TODO")
> +
> +
> +def get_interface_mac(ifname):
> +    """Returns the string value of an interface's MAC Address"""
> +    output = _ifconfig([ifname, 'ether'])
> +    mac_zero = '00:00:00:00:00:00'
> +    if output[0] is '' or output[1] is not '':
> +        return mac_zero
> +
> +    mac_re = re.compile("\s+ether (?P<mac>([a-f0-9]{2}:){5}[a-f0-9]{2})", re.I)
> +    ifcfg = output[0].split("\n")
> +    for l in ifcfg:
> +        maybe_mac = mac_re.match(l)
> +        if maybe_mac is not None:
> +            return maybe_mac.group('mac')
> +
> +    return mac_zero
> +
> +
> +def get_ib_interface_hwaddr(ifname, ethernet_format):
> +    """Since this isn't properly implemented yet, we're returning None for now
> +    rather than raise NotImplemented("TODO")
> +    That way, we can move get_interfaces_by_mac() into __init__"""
> +    return None

Heh, IB is an Infiniband networking interface.

> +
> +
> +
> +class EphemeralIPv4Network(object):
> +    """Context manager which sets up temporary static network configuration.
> +
> +    No operations are performed if the provided interface is already connected.
> +    If unconnected, bring up the interface with valid ip, prefix and broadcast.
> +    If router is provided setup a default route for that interface. Upon
> +    context exit, clean up the interface leaving no configuration behind.
> +    """
> +
> +    raise NotImplemented("TODO")
> +
> +
> +# vi: ts=4 expandtab


-- 
https://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358228
Your team cloud-init commiters is requested to review the proposed merge of ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master.


References