Skip to content
Open
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
44 changes: 44 additions & 0 deletions netbox_agent/ifconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import re
import subprocess


class Ifconfig:
"""Parse ``ifconfig -a`` output.

Used on systems without Linux sysfs (``/sys/class/net``) -- e.g. *BSD -- to
provide the per-interface facts that :class:`~netbox_agent.network.Network`
otherwise reads from ``/sys``: the hardware (MAC) address and the MTU.

Pass ``output`` to parse a captured string (used by the tests); otherwise it
runs ``ifconfig -a`` itself.
"""

def __init__(self, output=None):
if output is None:
output = subprocess.getoutput("ifconfig -a")
self.output = output
self.interfaces = self.parse()

def parse(self):
interfaces = {}
current = None
for line in self.output.splitlines():
# Interface header lines start in column 0, e.g.
# vtnet0: flags=1008843<UP,BROADCAST,...> metric 0 mtu 1500
header = re.match(r"^(\S+?): flags=\S*<[^>]*>(.*)$", line)
if header:
current = header.group(1)
mtu = re.search(r"\bmtu (\d+)", header.group(2))
interfaces[current] = {
"mac": None,
"mtu": int(mtu.group(1)) if mtu else None,
}
continue
if current is None:
continue
# Indented link-layer line carries the MAC, e.g.
# "\tether bc:24:11:6e:21:cd"
ether = re.match(r"\s+ether ([0-9a-fA-F:]{17})\b", line)
if ether:
interfaces[current]["mac"] = ether.group(1)
return interfaces
95 changes: 67 additions & 28 deletions netbox_agent/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from netbox_agent.config import config
from netbox_agent.config import netbox_instance as nb
from netbox_agent.ethtool import Ethtool
from netbox_agent.ifconfig import Ifconfig
from netbox_agent.ipmi import IPMI
from netbox_agent.lldp import LLDP

Expand Down Expand Up @@ -46,13 +47,69 @@ def __init__(self, server, *args, **kwargs):
def get_network_type():
return NotImplementedError

def scan(self):
nics = []
for interface in os.listdir("/sys/class/net/"):
def _use_sysfs(self):
"""Whether Linux sysfs (/sys/class/net) is available.

When it isn't (e.g. *BSD), interface facts come from ``ifconfig`` instead.
"""
return os.path.isdir("/sys/class/net")

def _ifconfig_interfaces(self):
"""Lazily parse ``ifconfig -a`` once, for the non-sysfs (BSD) code path."""
if not hasattr(self, "_ifconfig_cache"):
self._ifconfig_cache = Ifconfig().interfaces
return self._ifconfig_cache

def _interface_names(self):
if self._use_sysfs():
# ignore if it's not a link (ie: bonding_masters etc)
if not os.path.islink("/sys/class/net/{}".format(interface)):
continue
return [
i
for i in os.listdir("/sys/class/net/")
if os.path.islink("/sys/class/net/{}".format(i))
]
return list(self._ifconfig_interfaces().keys())

def _interface_mac(self, interface, ethtool):
if config.network.primary_mac == "permanent" and ethtool and ethtool.get("mac_address"):
mac = ethtool["mac_address"]
elif self._use_sysfs():
mac = open("/sys/class/net/{}/address".format(interface), "r").read().strip()
if mac == "00:00:00:00:00:00":
mac = None
else:
mac = self._ifconfig_interfaces().get(interface, {}).get("mac")
if mac == "00:00:00:00:00:00":
mac = None
if mac:
mac = mac.upper()
return mac

def _interface_mtu(self, interface):
if self._use_sysfs():
return int(open("/sys/class/net/{}/mtu".format(interface), "r").read().strip())
return self._ifconfig_interfaces().get(interface, {}).get("mtu")

def _interface_bonding(self, interface):
if self._use_sysfs() and os.path.isdir("/sys/class/net/{}/bonding".format(interface)):
slaves = open("/sys/class/net/{}/bonding/slaves".format(interface)).read().split()
return True, slaves
return False, []

def _interface_virtual(self, interface):
if self._use_sysfs():
return Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER
# No sysfs (e.g. *BSD): fall back to a name-based heuristic for the common
# virtual interface types.
return bool(
re.match(
r"^(lo|tun|tap|bridge|vlan|gif|gre|epair|pflog|pfsync|enc|ipfw)\d*$", interface
)
)

def scan(self):
nics = []
for interface in self._interface_names():
if config.network.ignore_interfaces and re.match(
config.network.ignore_interfaces, interface
):
Expand Down Expand Up @@ -90,33 +147,15 @@ def scan(self):
ip_addr.append(addr)

ethtool = Ethtool(interface).parse()
if (
config.network.primary_mac == "permanent"
and ethtool
and ethtool.get("mac_address")
):
mac = ethtool["mac_address"]
else:
mac = open("/sys/class/net/{}/address".format(interface), "r").read().strip()
if mac == "00:00:00:00:00:00":
mac = None
if mac:
mac = mac.upper()
mac = self._interface_mac(interface, ethtool)
mtu = self._interface_mtu(interface)

mtu = int(open("/sys/class/net/{}/mtu".format(interface), "r").read().strip())
vlan = None
if len(interface.split(".")) > 1:
vlan = int(interface.split(".")[1])

bonding = False
bonding_slaves = []
if os.path.isdir("/sys/class/net/{}/bonding".format(interface)):
bonding = True
bonding_slaves = (
open("/sys/class/net/{}/bonding/slaves".format(interface)).read().split()
)

virtual = Path(f"/sys/class/net/{interface}").resolve().parent == VIRTUAL_NET_FOLDER
bonding, bonding_slaves = self._interface_bonding(interface)
virtual = self._interface_virtual(interface)

nic = {
"name": interface,
Expand Down Expand Up @@ -555,7 +594,7 @@ def batched(it, n):
nic_update += 1

if hasattr(interface, "mtu"):
if nic["mtu"] != interface.mtu:
if nic["mtu"] and nic["mtu"] != interface.mtu:
logging.info(
"Interface mtu is wrong, updating to: {mtu}".format(mtu=nic["mtu"])
)
Expand Down
39 changes: 39 additions & 0 deletions tests/fixtures/ifconfig/freebsd_carp.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
vtnet0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
options=cc039a<TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,TSO6,VLAN_HWTSO,LINKSTATE,TXCSUM_IPV6,HWSTATS>
ether bc:24:11:6e:21:cd
inet 157.131.224.217 netmask 0xffffffc0 broadcast 157.131.224.255
media: Ethernet autoselect (10Gbase-T <full-duplex>)
status: active
nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
vtnet1: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
options=cc039a<TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,TSO6,VLAN_HWTSO,LINKSTATE,TXCSUM_IPV6,HWSTATS>
ether bc:24:11:90:23:4d
inet 10.0.6.2 netmask 0xffffff00 broadcast 10.0.6.255
inet 10.0.6.1 netmask 0xffffff00 broadcast 10.0.6.255 vhid 10
carp: MASTER vhid 10 advbase 1 advskew 100
peer 224.0.0.18 peer6 ff02::12
media: Ethernet autoselect (10Gbase-T <full-duplex>)
status: active
nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
lo0: flags=1008049<UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 16384
options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
groups: lo
nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
pfsync0: flags=1000041<UP,RUNNING,LOWER_UP> metric 0 mtu 1500
options=0
syncdev: vtnet1 maxupd: 128 defer: off version: 1500
syncok: 1
groups: pfsync
pflog0: flags=1000141<UP,RUNNING,PROMISC,LOWER_UP> metric 0 mtu 33152
options=0
groups: pflog
tailscale0: flags=1008043<UP,BROADCAST,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 1280
options=4080000<LINKSTATE,MEXTPG>
inet 100.78.225.91 netmask 0xffffffff broadcast 100.78.225.91
inet6 fd7a:115c:a1e0::ac01:e185 prefixlen 48
groups: tun
nd6 options=101<PERFORMNUD,NO_DAD>
Opened by PID 26613
21 changes: 21 additions & 0 deletions tests/network.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from netbox_agent.ifconfig import Ifconfig
from netbox_agent.lldp import LLDP
from tests.conftest import parametrize_with_fixtures

Expand Down Expand Up @@ -34,3 +35,23 @@ def test_lldp_parse_with_vlan(fixture):
lldp = LLDP(fixture)
assert lldp.get_switch_vlan("eth0") == {"300": {"pvid": True}}
assert lldp.get_switch_vlan("eth1") == {"300": {}}


@parametrize_with_fixtures(
"ifconfig/",
only_filenames=[
"freebsd_carp.txt",
],
)
def test_ifconfig_parse_freebsd(fixture):
interfaces = Ifconfig(fixture).interfaces
# MAC + MTU are picked up from the ether/header lines
assert interfaces["vtnet0"]["mac"] == "bc:24:11:6e:21:cd"
assert interfaces["vtnet0"]["mtu"] == 1500
assert interfaces["vtnet1"]["mac"] == "bc:24:11:90:23:4d"
assert interfaces["vtnet1"]["mtu"] == 1500
# interfaces without an ether line have no MAC, but still an MTU
assert interfaces["lo0"]["mac"] is None
assert interfaces["lo0"]["mtu"] == 16384
assert interfaces["pflog0"]["mtu"] == 33152
assert interfaces["tailscale0"]["mtu"] == 1280