yanger: Break up ietf-interfaces implementation into modules

This commit is contained in:
Tobias Waldekranz
2025-01-20 10:56:56 +01:00
parent e397012394
commit 78b7b34799
12 changed files with 631 additions and 575 deletions
-10
View File
@@ -1,15 +1,5 @@
LOG = None
def lookup(obj, *keys):
"""This function returns a value from a nested json object"""
curr = obj
for key in keys:
if isinstance(curr, dict) and key in curr:
curr = curr[key]
else:
return None
return curr
def insert(obj, *path_and_value):
""""This function inserts a value into a nested json object"""
+7 -5
View File
@@ -33,7 +33,7 @@ class Host(abc.ABC):
def run_multiline(self, cmd, default=None):
"""Get lines of stdout of cmd"""
try:
txt = self.run(cmd, log=(default is None))
txt = self.run(tuple(cmd), log=(default is None))
return txt.splitlines()
except:
if default is not None:
@@ -43,7 +43,7 @@ class Host(abc.ABC):
def run_json(self, cmd, default=None):
"""Get JSON object from stdout of cmd"""
try:
txt = self.run(cmd, log=(default is None))
txt = self.run(tuple(cmd), log=(default is None))
return json.loads(txt)
except:
if default is not None:
@@ -74,7 +74,7 @@ class Localhost(Host):
def now(self):
return datetime.datetime.now(tz=datetime.timezone.utc)
# @functools.cache
@functools.cache
def run(self, cmd, default=None, log=True):
try:
result = subprocess.run(cmd, check=True, text=True,
@@ -107,7 +107,7 @@ class Localhost(Host):
class Remotehost(Localhost):
def __init__(self, wrap, basedir):
super().__init__()
self.wrap = wrap.split()
self.wrap = tuple(wrap.split())
self.basedir = basedir
if basedir:
for subdir in ("rootfs", "run"):
@@ -146,7 +146,7 @@ class Remotehost(Localhost):
return out
def read(self, path):
out = self._run(["cat", path], default="", log=False)
out = self._run(("cat", path), default="", log=False)
if self.basedir:
dirname = os.path.join(self.basedir, "rootfs", os.path.dirname(path[1:]))
@@ -154,6 +154,8 @@ class Remotehost(Localhost):
with open(os.path.join(self.basedir, "rootfs", path[1:]), "w") as f:
f.write(out)
return out
class Testhost(Host):
def SlugOf(cmd):
@@ -1,564 +1,11 @@
from ..common import insert, lookup, LOG
from ..host import HOST
def json_get_yang_type(iface_in):
if iface_in['link_type'] == "loopback":
return "infix-if-type:loopback"
if iface_in['link_type'] in ("gre", "gre6"):
return "infix-if-type:gre"
if iface_in['link_type'] != "ether":
return "infix-if-type:other"
if 'parentbus' in iface_in and iface_in['parentbus'] == "virtio":
return "infix-if-type:etherlike"
if 'linkinfo' not in iface_in:
return "infix-if-type:ethernet"
if 'info_kind' not in iface_in['linkinfo']:
return "infix-if-type:ethernet"
if iface_in['linkinfo']['info_kind'] == "veth":
return "infix-if-type:veth"
if iface_in['linkinfo']['info_kind'] in ("gretap", "ip6gretap"):
return "infix-if-type:gretap"
if iface_in['linkinfo']['info_kind'] == "vlan":
return "infix-if-type:vlan"
if iface_in['linkinfo']['info_kind'] == "bridge":
return "infix-if-type:bridge"
if iface_in['linkinfo']['info_kind'] == "dsa":
return "infix-if-type:ethernet"
if iface_in['linkinfo']['info_kind'] == "dummy":
return "infix-if-type:dummy"
# Fallback
return "infix-if-type:ethernet"
def json_get_yang_origin(addr):
"""Translate kernel IP address origin to YANG"""
xlate = {
"kernel_ll": "link-layer",
"kernel_ra": "link-layer",
"static": "static",
"dhcp": "dhcp",
"random": "random",
}
proto = addr['protocol']
if proto in ("kernel_ll", "kernel_ra"):
if "stable-privacy" in addr:
return "random"
return xlate.get(proto, "other")
def iface_is_dsa(iface_in):
"""Check if interface is a DSA/intra-switch port"""
if "linkinfo" not in iface_in:
return False
if "info_kind" not in iface_in['linkinfo']:
return False
if iface_in['linkinfo']['info_kind'] != "dsa":
return False
return True
def get_bridge_port_pvid(ifname):
data = HOST.run_json(['bridge', '-j', 'vlan', 'show', 'dev', ifname])
if len(data) != 1:
return None
iface = data[0]
for vlan in iface['vlans']:
if 'flags' in vlan and 'PVID' in vlan['flags']:
return vlan['vlan']
return None
def get_bridge_port_stp_state(ifname):
data = HOST.run_json(['bridge', '-j', 'link', 'show', 'dev', ifname])
if len(data) != 1:
return None
iface = data[0]
states = ['disabled', 'listening', 'learning', 'forwarding', 'blocking']
if 'state' in iface and iface['state'] in states:
return iface['state']
return None
def get_brport_multicast(ifname):
"""Check if multicast snooping is enabled on bridge, default: nope"""
data = HOST.run_json(['mctl', '-p', 'show', 'igmp', 'json'], default={})
multicast = {}
if ifname in data.get('fast-leave-ports', []):
multicast["fast-leave"] = True
else:
multicast["fast-leave"] = False
if ifname in data.get('multicast-router-ports', []):
multicast["router"] = "permanent"
else:
multicast["router"] = "auto"
return multicast
# We always get all interfaces for two reasons.
# 1) To increase speed on large iron with many ports.
# 2) To simplify testing (single dummy file ip-link-show.json).
def get_ip_link():
"""Fetch interface link information from kernel"""
return HOST.run_json(['ip', '-s', '-d', '-j', 'link', 'show'])
def netns_get_ip_link(netns):
"""Fetch interface link information from within a network namespace"""
return HOST.run_json(['ip', 'netns', 'exec', netns, 'ip', '-s', '-d', '-j', 'link', 'show'])
def get_ip_addr():
"""Fetch interface address information from kernel"""
return HOST.run_json(['ip', '-j', 'addr', 'show'])
def netns_get_ip_addr(netns):
"""Fetch interface address information from within a network namespace"""
return HOST.run_json(['ip', 'netns', 'exec', netns, 'ip', '-j', 'addr', 'show'])
def get_netns_list():
"""Fetch a list of network namespaces"""
return HOST.run_json(['ip', '-j', 'netns', 'list'], [])
def netns_find_ifname(ifname):
"""Find which network namespace owns ifname (if any)"""
for netns in get_netns_list():
for iface in netns_get_ip_link(netns['name']):
if 'ifalias' in iface and iface['ifalias'] == ifname:
return netns['name']
return None
def netns_ifindex_to_ifname(ifindex):
"""Look through all network namespaces for an interface index and return its name"""
for netns in get_netns_list():
for iface in netns_get_ip_link(netns['name']):
if iface['ifindex'] == ifindex:
if 'ifalias' in iface:
return iface['ifalias']
if 'ifname' in iface:
return iface['ifname']
return None
return None
def add_bridge_port_common(ifname, iface_in, iface_out):
li = iface_in.get("linkinfo", {})
if not (li.get("info_slave_kind") == "bridge" or \
li.get("info_kind") == "bridge"):
return
pvid = get_bridge_port_pvid(ifname)
if pvid is not None:
insert(iface_out, "infix-interfaces:bridge-port", "pvid", pvid)
def add_bridge_port_lower(ifname, iface_in, iface_out):
li = iface_in.get("linkinfo", {})
if not li.get("info_slave_kind") == "bridge":
return
insert(iface_out, "infix-interfaces:bridge-port", "bridge", iface_in['master'])
stp_state = get_bridge_port_stp_state(ifname)
if stp_state is not None:
insert(iface_out, "infix-interfaces:bridge-port", "stp-state", stp_state)
multicast = get_brport_multicast(ifname)
insert(iface_out, "infix-interfaces:bridge-port", "multicast", multicast)
def add_gre(iface_in, iface_out):
if 'link_type' in iface_in:
val = json_get_yang_type(iface_in)
if val != "infix-if-type:gre" and val != "infix-if-type:gretap":
return;
gre={}
info_data=iface_in.get("linkinfo", {}).get("info_data", {})
gre["local"] = info_data.get("local")
gre["remote"] = info_data.get("remote")
insert(iface_out, "infix-interfaces:gre", gre)
def add_ip_link(ifname, iface_in, iface_out):
if 'ifname' in iface_in:
iface_out['name'] = ifname
if 'ifindex' in iface_in:
iface_out['if-index'] = iface_in['ifindex']
if 'ifalias' in iface_in:
iface_out['description'] = iface_in['ifalias']
if 'address' in iface_in and not "POINTOPOINT" in iface_in["flags"]:
iface_out['phys-address'] = iface_in['address']
add_bridge_port_common(ifname, iface_in, iface_out)
add_bridge_port_lower(ifname, iface_in, iface_out)
if not iface_is_dsa(iface_in):
if iface_in.get('link'):
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", iface_in['link'])
elif 'link_index' in iface_in:
# 'link_index' is the only reference we have if the link iface is in a namespace
lower = netns_ifindex_to_ifname(iface_in['link_index'])
if lower:
insert(iface_out, "infix-interfaces:vlan", "lower-layer-if", lower)
if 'flags' in iface_in:
iface_out['admin-status'] = "up" if "UP" in iface_in['flags'] else "down"
if 'operstate' in iface_in:
xlate = {
"DOWN": "down",
"UP": "up",
"DORMANT": "dormant",
"TESTING": "testing",
"LOWERLAYERDOWN": "lower-layer-down",
"NOTPRESENT": "not-present"
}
val = xlate.get(iface_in['operstate'], "unknown")
iface_out['oper-status'] = val
if 'link_type' in iface_in:
val = json_get_yang_type(iface_in)
iface_out['type'] = val
add_gre(iface_in, iface_out)
val = lookup(iface_in, "stats64", "rx", "bytes")
if val is not None:
insert(iface_out, "statistics", "out-octets", str(val))
val = lookup(iface_in, "stats64", "tx", "bytes")
if val is not None:
insert(iface_out, "statistics", "in-octets", str(val))
def add_ip_addr(ifname, iface_in, iface_out):
if 'mtu' in iface_in and ifname != "lo":
insert(iface_out, "ietf-ip:ipv4", "mtu", iface_in['mtu'])
val = HOST.read(f"/proc/sys/net/ipv6/conf/{ifname}/mtu")
if val is not None:
insert(iface_out, "ietf-ip:ipv6", "mtu", int(val.strip()))
if 'addr_info' in iface_in:
inet = []
inet6 = []
for addr in iface_in['addr_info']:
new = {}
if 'family' not in addr:
LOG.error("'family' missing from 'addr_info'")
continue
if 'local' in addr:
new['ip'] = addr['local']
if 'prefixlen' in addr:
new['prefix-length'] = addr['prefixlen']
if 'protocol' in addr:
new['origin'] = json_get_yang_origin(addr)
if addr['family'] == "inet":
inet.append(new)
elif addr['family'] == "inet6":
inet6.append(new)
else:
LOG.error("invalid 'family' in 'addr_info'")
sys.exit(1)
insert(iface_out, "ietf-ip:ipv4", "address", inet)
insert(iface_out, "ietf-ip:ipv6", "address", inet6)
def add_ethtool_groups(ifname, iface_out):
"""Fetch interface counters from kernel (need new JSON format!)"""
cmd = ['ethtool', '--json', '-S', ifname, '--all-groups']
try:
data = HOST.run_json(cmd)
if len(data) != 1:
LOG.warning("%s: no counters available, skipping.", ifname)
return
except subprocess.CalledProcessError:
# Allow comand to fail, not all NICs support --json yet
return
iface_in = data[0]
# TODO: room for improvement, the "frame" creation could be more dynamic.
if "eth-mac" in iface_in or "rmon" in iface_in:
insert(iface_out, "ieee802-ethernet-interface:ethernet", "statistics", "frame", {})
frame = iface_out['ieee802-ethernet-interface:ethernet']['statistics']['frame']
if "eth-mac" in iface_in:
mac_in = iface_in['eth-mac']
if "FramesTransmittedOK" in mac_in:
frame['out-frames'] = str(mac_in['FramesTransmittedOK'])
if "MulticastFramesXmittedOK" in mac_in:
frame['out-multicast-frames'] = str(mac_in['MulticastFramesXmittedOK'])
if "BroadcastFramesXmittedOK" in mac_in:
frame['out-broadcast-frames'] = str(mac_in['BroadcastFramesXmittedOK'])
if "FramesReceivedOK" in mac_in:
frame['in-frames'] = str(mac_in['FramesReceivedOK'])
if "MulticastFramesReceivedOK" in mac_in:
frame['in-multicast-frames'] = str(mac_in['MulticastFramesReceivedOK'])
if "BroadcastFramesReceivedOK" in mac_in:
frame['in-broadcast-frames'] = str(mac_in['BroadcastFramesReceivedOK'])
if "FrameCheckSequenceErrors" in mac_in:
frame['in-error-fcs-frames'] = str(mac_in['FrameCheckSequenceErrors'])
if "FramesLostDueToIntMACRcvError" in mac_in:
frame['in-error-mac-internal-frames'] = str(mac_in['FramesLostDueToIntMACRcvError'])
if "OctetsTransmittedOK" in mac_in:
frame['infix-ethernet-interface:out-good-octets'] = str(mac_in['OctetsTransmittedOK'])
if "OctetsReceivedOK" in mac_in:
frame['infix-ethernet-interface:in-good-octets'] = str(mac_in['OctetsReceivedOK'])
tot = 0
found = False
if "FramesReceivedOK" in mac_in:
tot += mac_in['FramesReceivedOK']
found = True
if "FrameCheckSequenceErrors" in mac_in:
tot += mac_in['FrameCheckSequenceErrors']
found = True
if "FramesLostDueToIntMACRcvError" in mac_in:
tot += mac_in['FramesLostDueToIntMACRcvError']
found = True
if "AlignmentErrors" in mac_in:
tot += mac_in['AlignmentErrors']
found = True
if "etherStatsOversizePkts" in mac_in:
tot += mac_in['etherStatsOversizePkts']
found = True
if "etherStatsJabbers" in mac_in:
tot += mac_in['etherStatsJabbers']
found = True
if found:
frame['in-total-frames'] = str(tot)
if "rmon" in iface_in:
rmon_in = iface_in['rmon']
if "undersize_pkts" in rmon_in:
frame['in-error-undersize-frames'] = str(rmon_in['undersize_pkts'])
tot = 0
found = False
if "etherStatsJabbers" in rmon_in:
tot += rmon_in['etherStatsJabbers']
found = True
if "etherStatsOversizePkts" in rmon_in:
tot += rmon_in['etherStatsOversizePkts']
found = True
if found:
frame['in-error-oversize-frames'] = str(tot)
def add_ethtool_std(ifname, iface_out):
"""Fetch interface speed/duplex/autoneg from kernel"""
keys = ['Speed', 'Duplex', 'Auto-negotiation']
result = {}
lines = HOST.run_multiline(['ethtool', ifname])
for line in lines:
line = line.strip()
key = line.split(':', 1)[0].strip()
if key in keys:
key, value = line.split(':', 1)
result[key.strip()] = value.strip()
if "Auto-negotiation" in result:
if result['Auto-negotiation'] == "on":
insert(iface_out, "ieee802-ethernet-interface:ethernet", "auto-negotiation", "enable", True)
else:
insert(iface_out, "ieee802-ethernet-interface:ethernet", "auto-negotiation", "enable", False)
if "Duplex" in result:
if result['Duplex'] == "Half":
insert(iface_out, "ieee802-ethernet-interface:ethernet", "duplex", "half")
elif result['Duplex'] == "Full":
insert(iface_out, "ieee802-ethernet-interface:ethernet", "duplex", "full")
else:
insert(iface_out, "ieee802-ethernet-interface:ethernet", "duplex", "unknown")
if "Speed" in result and result['Speed'] != "Unknown!":
# Avoid importing re (performance)
num = ''.join(filter(str.isdigit, result['Speed']))
if num:
num = round((int(num) / 1000), 3)
insert(iface_out, "ieee802-ethernet-interface:ethernet", "speed", str(num))
def get_querier_data(querier_data):
multicast={}
if not querier_data:
multicast["snooping"] = False
return multicast
multicast["snooping"] = True
if(querier_data.get("query-interval")):
multicast["query-interval"] = querier_data["query-interval"]
return multicast
def get_multicast_filters(filters):
multicast_filters=[]
for f in filters:
multicast_filter={}
multicast_filter["group"] = f["group"]
multicast_filter["ports"] = []
for p in f["ports"]:
port={}
port["port"] = p
multicast_filter["ports"].append(port)
multicast_filters.append(multicast_filter)
return multicast_filters
def add_mdb_to_bridge(brname, iface_out, mc_status):
filters = [entry for entry in mc_status.get("multicast-groups", []) if entry.get('vid') == None and entry.get('bridge') == brname]
querier = next((querier for querier in mc_status.get('multicast-queriers', []) if (querier.get("interface", "") == brname) and (querier.get("vid") is None)), None)
multicast = get_querier_data(querier)
multicast_filters = get_multicast_filters(filters)
insert(iface_out, "infix-interfaces:bridge", "multicast", multicast)
insert(iface_out, "infix-interfaces:bridge", "multicast-filters", "multicast-filter", multicast_filters)
def add_container_ifaces(yang_ifaces):
"""Add all podman interfaces with limited data"""
interfaces={}
try:
containers = HOST.run_json(['podman', 'ps', '-a', '--format=json'], default=[])
except Exception as e:
logging.error(f"Error, unable to run podman: {e}")
return
for container in containers:
name = container.get('Names', ['Unknown'])[0]
networks = container.get('Networks', [])
for network in networks:
if not network in interfaces:
interfaces[network] = []
if name not in interfaces[network]:
interfaces[network].append(name)
for ifname, containers in interfaces.items():
iface_out = {}
iface_out['name'] = ifname
iface_out['type'] = "infix-if-type:other" # Fallback
insert(iface_out, "infix-interfaces:container-network", "containers", containers)
netns = netns_find_ifname(ifname)
if netns is not None:
ip_link_data = netns_get_ip_link(netns)
ip_link_data = next((d for d in ip_link_data if d.get('ifalias') == ifname), None)
add_ip_link(ifname, ip_link_data, iface_out)
yang_ifaces.append(iface_out)
# Helper function to add tagged/untagged interfaces to a vlan dict in a list
def _add_vlan_iface(vlans, multicast_filter, multicast, vid, key, val):
for d in vlans:
if d['vid'] == vid:
if key in d:
d[key].append(val)
else:
d[key] = [val]
return
vlans.append({'vid': vid, "multicast-filters": {"multicast-filter": multicast_filter}, "multicast": multicast, key: [val]})
def add_vlans_to_bridge(brname, iface_out, mc_status):
slaves = [] # Contains all interfaces that has this bridge as 'master'
for iface in HOST.run_json(['bridge', '-j', 'link']):
if "master" in iface and iface['master'] == brname:
slaves.append(iface['ifname'])
vlans = [] # Contains all vlans and slaves belonging to this bridge
for iface in HOST.run_json(['bridge', '-j', 'vlan']):
if iface['ifname'] not in slaves and iface['ifname'] != brname:
continue
for vlan in iface['vlans']:
querier = next((querier for querier in mc_status.get('multicast-queriers', []) if (querier.get("vid") == vlan['vlan'])), None)
filters = [entry for entry in mc_status.get("multicast-groups", []) if (entry.get("vid") == vlan["vlan"])]
if 'flags' in vlan and 'Egress Untagged' in vlan['flags']:
_add_vlan_iface(vlans, get_multicast_filters(filters), get_querier_data(querier), vlan['vlan'], 'untagged', iface['ifname'])
else:
_add_vlan_iface(vlans, get_multicast_filters(filters), get_querier_data(querier), vlan['vlan'], 'tagged', iface['ifname'])
insert(iface_out, "infix-interfaces:bridge", "vlans", "vlan", vlans)
def get_iface_data(ifname, ip_link_data, ip_addr_data):
iface_out = {}
add_ip_link(ifname, ip_link_data, iface_out)
add_ip_addr(ifname, ip_addr_data, iface_out)
if 'type' in iface_out and iface_out['type'] == "infix-if-type:ethernet":
add_ethtool_groups(ifname, iface_out)
add_ethtool_std(ifname, iface_out)
if 'type' in iface_out and iface_out['type'] == "infix-if-type:bridge":
# Fail silent, multicast snooping may not be enabled on bridge
mc_status = HOST.run_json(['mctl', '-p', 'show', 'igmp', 'json'], default={})
add_vlans_to_bridge(ifname, iface_out, mc_status)
add_mdb_to_bridge(ifname, iface_out, mc_status)
return iface_out
def _add_interface(ifname, ip_link_data, ip_addr_data, yang_ifaces):
# We expect both ip addr and link data to exist.
if not ip_link_data or not ip_addr_data:
return
# Skip internal interfaces.
if 'group' in ip_link_data and ip_link_data['group'] == "internal":
return
yang_ifaces.append(get_iface_data(ifname, ip_link_data, ip_addr_data))
from . import container
from . import link
def operational(ifname=None):
out = {
return {
"ietf-interfaces:interfaces": {
"interface": []
}
"interface":
link.interfaces(ifname) +
container.interfaces(ifname),
},
}
out_ifaces = out["ietf-interfaces:interfaces"]["interface"]
ip_link_data = get_ip_link()
ip_addr_data = get_ip_addr()
if ifname:
ip_link_data = next((d for d in ip_link_data if d.get('ifname') == ifname), None)
ip_addr_data = next((d for d in ip_addr_data if d.get('ifname') == ifname), None)
_add_interface(ifname, ip_link_data, ip_addr_data, out_ifaces)
else:
for link in ip_link_data:
addr = next((d for d in ip_addr_data if d.get('ifname') == link["ifname"]), None)
_add_interface(link["ifname"], link, addr, out_ifaces)
add_container_ifaces(out_ifaces)
return out
@@ -0,0 +1,171 @@
from functools import cache
from ..common import LOG
from ..host import HOST
from .common import iplinks, iplinks_lower_of
from . import vlan
@cache
def bridge_vlan():
return { v["ifname"]: v for v in HOST.run_json("bridge -j vlan show".split(), []) }
def lower(iplink):
lower = {}
if brpvlans := bridge_vlan().get(iplink["ifname"]):
for brpvlan in brpvlans["vlans"]:
if "PVID" in brpvlan.get("flags", []):
lower["pvid"] = brpvlan["vlan"]
if iplink.get("linkinfo", {}).get("info_kind") == "bridge":
return lower
info = iplink["linkinfo"]["info_slave_data"]
return lower | {
"bridge": iplink["master"],
"flood": {
"broadcast": info["bcast_flood"],
"unicast": info["flood"],
"multicast": info["mcast_flood"],
},
"multicast": {
"fast-leave": info["fastleave"],
"router": {
0: "off",
1: "auto",
2: "permanent",
}.get(info["multicast_router"], "UNKNOWN"),
},
"stp-state": info["state"],
}
def mctlq2yang_mode(mctlq):
if state := mctlq.get("state"):
return "proxy" if state == "proxy" else "auto"
return "off"
def mctl(ifname, vid):
mctl = HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={})
for q in mctl.get("multicast-queriers", []):
# TODO: Also need to match against VLAN uppers (e.g. br0.1337)
if q.get("interface") == ifname and q.get("vid") == vid:
return q
return {}
def multicast_filters(iplink, vid):
filt = ["dev", iplink["ifname"]] + (["vid", str(vid)] if vid else [])
brmdb = HOST.run_json(["bridge", "-j", "mdb", "show"] + filt)[0]["mdb"]
mdb = {}
for brentry in brmdb:
if not (entry := mdb.get(brentry["grp"])):
mdb[brentry["grp"]] = {
"group": brentry["grp"],
"ports": [],
}
entry = mdb[brentry["grp"]]
entry["ports"].append({
"port": brentry["port"],
"state": {
"temp": "temporary",
"permanent": "permanent"
}.get(brentry["state"], "UNKNOWN"),
})
return { "multicast-filter": list(mdb.values()) }
def multicast(iplink, info):
mctlq = mctl(iplink["ifname"], info.get("vlan"))
mcast = {
"snooping": bool(info.get("mcast_snooping")),
"querier": mctlq2yang_mode(mctlq),
}
if interval := mctlq.get("interval"):
mcast["query-interval"] = interval
return mcast
def vlans_add_memberships(iplink, vlans):
brvlans = bridge_vlan()
ports = [iplink["ifname"]] + [link["ifname"] for link in iplinks_lower_of(iplink["ifname"]).values()]
for port in ports:
if not (brpvlans := brvlans.get(port)):
continue
for brpvlan in brpvlans["vlans"]:
if not (vlan := vlans.get(brpvlan["vlan"])):
LOG.error(f"Unexpected vlan {brpvlans['vlan']} on {port}")
continue
if "Egress Untagged" in brpvlan.get("flags", []):
vlan["untagged"].append(port)
else:
vlan["tagged"].append(port)
def vlans(iplink):
if not (brgvlans := HOST.run_json(f"bridge -j vlan global show dev {iplink['ifname']}".split())):
return []
vlans = {
v["vlan"]: {
"vid": v["vlan"],
"untagged": [],
"tagged": [],
"multicast": multicast(iplink, v),
"multicast-filters": multicast_filters(iplink, v["vlan"]),
}
for v in brgvlans[0]["vlans"]
}
vlans_add_memberships(iplink, vlans)
return list(vlans.values())
def qbridge(iplink):
info = iplink["linkinfo"]["info_data"]
return {
"vlans": {
"proto": vlan.proto2yang(info["vlan_protocol"]),
"vlan": vlans(iplink),
}
}
def dbridge(iplink):
info = iplink["linkinfo"]["info_data"]
return {
"multicast": multicast(iplink, info),
"multicast-filters": multicast_filters(iplink, None),
}
def bridge(iplink):
info = iplink["linkinfo"]["info_data"]
if info.get("vlan_filtering"):
return qbridge(iplink)
else:
return dbridge(iplink)
@@ -0,0 +1,30 @@
from functools import cache
from ..host import HOST
@cache
def iplinks(ifname=None, netns=None):
def _iplinks(ifname, netns):
pre = [ "ip", "netns", "exec", netns ] if netns else []
filt = ["dev", ifname] if ifname else []
return HOST.run_json(pre + ["ip", "-s", "-d", "-j", "link", "show"] + filt)
return { link["ifname"]: link for link in _iplinks(ifname, netns) }
def iplinks_lower_of(upper):
return {
link["ifname"]: link for link in
filter(lambda link: link.get("master") == upper, iplinks().values())
}
@cache
def ipaddrs(ifname=None, netns=None):
def _ipaddrs(ifname, netns):
pre = [ "ip", "netns", "exec", netns ] if netns else []
filt = ["dev", ifname] if ifname else []
return HOST.run_json(pre + ["ip", "-j", "addr", "show"] + filt)
return { addr["ifname"]: addr for addr in _ipaddrs(ifname, netns) }
@@ -0,0 +1,57 @@
from ..host import HOST
from ..infix_containers import podman_ps
from . import common
from . import link
def ip_netns_list():
return HOST.run_json(["ip", "-j", "netns", "list"], [])
def find_interface(cifname):
for ns in ip_netns_list():
for iplink in common.iplinks(netns=ns["name"]).values():
if iplink.get("ifalias") == cifname:
ipaddrs = common.ipaddrs(ifname=iplink["ifname"], netns=ns["name"])
return (iplink, next(iter(ipaddrs.values())))
def podman_interfaces():
interfaces = {}
for container in podman_ps():
containername = container.get("Names", ["Unknown"])[0]
for ifname in container.get("Networks", []):
if ifname not in interfaces:
interfaces[ifname] = []
if containername not in interfaces[ifname]:
interfaces[ifname].append(containername)
return interfaces
def interfaces(ifname):
interfaces = []
for cifname, cnames in podman_interfaces().items():
if ifname and cifname != ifname:
continue
iplink, ipaddr = find_interface(cifname)
interface = link.interface_common(iplink, ipaddr)
# The original interface name is stored in ifalias by podman -
# which we then translate to the description. We need to
# reverse these since the "name" from the user's perspective
# is the one set in running-config, not whatever the container
# has renamed it to.
interface["description"] = interface["name"]
interface["name"] = cifname
interface["infix-interfaces:container-network"] = {
"containers": cnames,
}
interfaces.append(interface)
return interfaces
@@ -0,0 +1,110 @@
from ..host import HOST
def frame_statistics(etstats):
STAT_MAP = {
"eth-mac": {
"out-frames": "FramesTransmittedOK",
"out-multicast-frames": "MulticastFramesXmittedOK",
"out-broadcast-frames": "BroadcastFramesXmittedOK",
"in-frames": "FramesReceivedOK",
"in-multicast-frames": "MulticastFramesReceivedOK",
"in-broadcast-frames": "BroadcastFramesReceivedOK",
"in-error-fcs-frames": "FrameCheckSequenceErrors",
"in-error-mac-internal-frames": "FramesLostDueToIntMACRcvError",
"infix-ethernet-interface:out-good-octets": "OctetsTransmittedOK",
"infix-ethernet-interface:in-good-octets": "OctetsReceivedOK",
"in-total-frames": (
"FramesReceivedOK",
"FrameCheckSequenceErrors",
"FramesLostDueToIntMACRcvError",
"AlignmentErrors",
"etherStatsOversizePkts",
"etherStatsJabbers",
),
},
"rmon": {
"in-error-undersize-frames": "undersize_pkts",
"in-error-oversize-frames": (
"etherStatsJabbers",
"etherStatsOversizePkts",
),
},
}
fstats = {}
for group, mapping in STAT_MAP.items():
etgroup = etstats.get(group)
if not etgroup:
continue
for name, source in mapping.items():
if type(source) == str:
counter = etgroup.get(source)
if counter is not None:
fstats[name] = str(counter)
elif type(source) == tuple:
inputs = [etgroup.get(src) for src in source]
if inputs := filter(lambda i: i is not None, inputs):
fstats[name] = str(sum(inputs))
return fstats
def statistics(ifname):
if etstats := HOST.run_json(["ethtool", "--json", "-S", ifname, "--all-groups"], []):
etstats = etstats[0]
else:
return None
statistics = {}
if fstats := frame_statistics(etstats):
statistics["frame"] = fstats
return statistics
def link(ethtool):
"""Parse speed/duplex/autoneg from ethtool output"""
eth = {}
for line in ethtool:
kv = [s.strip() for s in line.split(":")]
if len(kv) != 2:
continue
key, val = kv
match key:
case "Auto-negotiation":
eth["auto-negotiation"] = { "enable": val == "on" }
case "Duplex":
match val:
case "Half":
eth["duplex"] = "half"
case "Full":
eth["duplex"] = "full"
case _:
eth["duplex"] = "unknown"
case "Speed":
mbps = "".join(filter(str.isdigit, val))
if mbps:
gbps = round((int(mbps) / 1000), 3)
eth["speed"] = str(gbps)
return eth
def ethernet(iplink):
ethtool = HOST.run_multiline(["ethtool", iplink["ifname"]])
eth = link(ethtool)
if stats := statistics(iplink["ifname"]):
eth["statistics"] = stats
return eth
@@ -0,0 +1,56 @@
from ..host import HOST
def inet2yang_origin(inet):
"""Translate kernel IP address origin to YANG"""
xlate = {
"kernel_ll": "link-layer",
"kernel_ra": "link-layer",
"static": "static",
"dhcp": "dhcp",
"random": "random",
}
proto = inet.get("protocol")
if proto in ("kernel_ll", "kernel_ra"):
if "stable-privacy" in inet:
return "random"
return xlate.get(proto, "other")
def addresses(ipaddr, proto):
addrs = []
for inet in ipaddr.get("addr_info", []):
if inet.get("family") != proto:
continue
addrs.append({
"ip": inet.get("local"),
"prefix-length": inet.get("prefixlen"),
"origin": inet2yang_origin(inet),
})
return addrs
def ipv4(ipaddr):
ipv4 = {}
mtu = ipaddr.get("mtu")
if mtu and ipaddr.get("ifname") != "lo":
ipv4["mtu"] = mtu
if addrs := addresses(ipaddr, "inet"):
ipv4["address"] = addrs
return ipv4
def ipv6(ipaddr):
ipv6 = {}
if mtu := HOST.read(f"/proc/sys/net/ipv6/conf/{ipaddr['ifname']}/mtu"):
ipv6["mtu"] = int(mtu.strip())
if addrs := addresses(ipaddr, "inet6"):
ipv6["address"] = addrs
return ipv6
@@ -0,0 +1,158 @@
from . import common
from . import bridge
from . import ethernet
from . import ip
from . import tun
from . import veth
from . import vlan
def statistics(iplink):
statistics = {}
if rx := iplink.get("stats64", {}).get("rx"):
if octets := rx.get("bytes"):
statistics["in-octets"] = str(octets)
if tx := iplink.get("stats64", {}).get("tx"):
if octets := tx.get("bytes"):
statistics["out-octets"] = str(octets)
return statistics
def iplink2yang_type(iplink):
match iplink["link_type"]:
case "loopback":
return "infix-if-type:loopback"
case "gre"|"gre6":
return "infix-if-type:gre"
case "ether":
pass
case _:
return "infix-if-type:other"
if iplink.get("parentbus") == "virtio":
return "infix-if-type:etherlike"
match iplink.get("linkinfo", {}).get("info_kind"):
case "bond":
return "infix-if-type:lag"
case "bridge":
return "infix-if-type:bridge"
case "dummy":
return "infix-if-type:dummy"
case "gretap"|"ip6gretap":
return "infix-if-type:gretap"
case "veth":
return "infix-if-type:veth"
case "vlan":
return "infix-if-type:vlan"
return "infix-if-type:ethernet"
def iplink2yang_lower(iplink):
if not (kind := iplink.get("linkinfo",{}).get("info_slave_kind")):
return None
match kind:
case "bridge":
return "infix-interfaces:bridge-port"
case "bond":
return "infix-interfaces:lag-port"
return None
def iplink2yang_operstate(iplink):
xlate = {
"DOWN": "down",
"UP": "up",
"DORMANT": "dormant",
"TESTING": "testing",
"LOWERLAYERDOWN": "lower-layer-down",
"NOTPRESENT": "not-present"
}
return xlate.get(iplink["operstate"], "unknown")
def interface_common(iplink, ipaddr):
interface = {
"type": iplink2yang_type(iplink),
"name": iplink.get("ifname"),
"if-index": iplink.get("ifindex"),
"admin-status": "up" if "UP" in iplink.get("flags", "") else "down",
"oper-status": iplink2yang_operstate(iplink),
}
if "ifalias" in iplink:
interface["description"] = iplink["ifalias"]
if "address" in iplink and not "POINTOPOINT" in iplink["flags"]:
interface["phys-address"] = iplink["address"]
if stats := statistics(iplink):
interface["statistics"] = stats
if ipv4 := ip.ipv4(ipaddr):
interface["ietf-ip:ipv4"] = ipv4
if ipv6 := ip.ipv6(ipaddr):
interface["ietf-ip:ipv6"] = ipv6
return interface
def interface(iplink, ipaddr):
interface = interface_common(iplink, ipaddr)
match interface["type"]:
case "infix-if-type:bridge":
if br := bridge.bridge(iplink):
interface["infix-interfaces:bridge"] = br
if brport := bridge.lower(iplink):
interface["infix-interfaces:bridge-port"] = brport
# case "infix-if-type:lag":
# if l := lag.lag(iplink):
# interface["infix-interfaces:lag"] = l
case "infix-if-type:ethernet":
if eth := ethernet.ethernet(iplink):
interface["ieee802-ethernet-interface:ethernet"] = eth
case "infix-if-type:gre"|"infix-if-type:gretap":
if gre := tun.gre(iplink):
interface["infix-interfaces:gre"] = gre
case "infix-if-type:veth":
if ve := veth.veth(iplink):
interface["infix-interfaces:veth"] = ve
case "infix-if-type:vlan":
if v := vlan.vlan(iplink):
interface["infix-interfaces:vlan"] = v
match iplink2yang_lower(iplink):
case "infix-interfaces:bridge-port":
if brport := bridge.lower(iplink):
interface["infix-interfaces:bridge-port"] = brport
# case "infix-interfaces:lag-port":
# if lagport := lag.lower(iplink):
# interface["infix-interfaces:lag-port"] = lagport
return interface
def interfaces(ifname=None):
links = common.iplinks(ifname)
addrs = common.ipaddrs(ifname)
interfaces = []
for ifname, iplink in links.items():
if iplink.get("group") == "internal":
continue
ipaddr = addrs.get(ifname, {})
interfaces.append(interface(iplink, ipaddr))
return interfaces
@@ -0,0 +1,7 @@
def gre(iplink):
info=iplink.get("linkinfo", {}).get("info_data", {})
return {
"local": info["local"],
"remote": info["remote"],
}
@@ -0,0 +1,7 @@
def veth(iplink):
veth = {}
if peer := iplink.get("link"):
veth["peer"] = peer
return veth
@@ -0,0 +1,21 @@
def proto2yang(proto):
return {
"802.1Q": "ieee802-dot1q-types:c-vlan",
"802.1ad": "ieee802-dot1q-types:s-vlan",
}.get(proto, "other")
def vlan(iplink):
info = iplink["linkinfo"]["info_data"]
vlan = {
"tag-type": proto2yang(info["protocol"]),
"id": info["id"],
}
# Lower could be in a different namespace, and thus might not be
# available to us
if lower := iplink.get("link"):
vlan["lower-layer-if"] = lower
return vlan