Files
infix/src/statd/python/yanger/yanger.py
T
Tobias Waldekranz 0c88c464f1 statd: Support reporting PVID on bridges
Now that PVID can be configured on bridges, make sure that we reflect
that in the operational database.
2024-11-28 12:52:09 +01:00

1145 lines
40 KiB
Python
Executable File

#!/usr/bin/env python3
import logging
import logging.handlers
import subprocess
import json
import sys # (built-in module)
import os
import argparse
from re import match
from datetime import datetime, timedelta, timezone
TESTPATH = ""
logger = None
def datetime_now():
if TESTPATH:
return datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
return datetime.now(timezone.utc)
def json_get_yang_type(iface_in):
if iface_in['link_type'] == "loopback":
return "infix-if-type:loopback"
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'] == "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 get_proc_value(procfile):
"""Return contents of /proc file, or None"""
try:
with open(procfile, 'r') as file:
data = file.read().strip()
return data
except FileNotFoundError:
# This is considered OK
return None
except IOError:
logger.error(f"failed reading from {procfile}")
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"""
if len(path_and_value) < 2:
raise ValueError("Error: insert() takes at least two args")
*path, value = path_and_value
curr = obj
for key in path[:-1]:
if key not in curr or not isinstance(curr[key], dict):
curr[key] = {}
curr = curr[key]
curr[path[-1]] = value
def run_cmd(cmd, testfile, default=None, check=True):
"""Run a command (array of args) and return an array of lines"""
if TESTPATH and testfile:
cmd = ['cat', os.path.join(TESTPATH, testfile)]
try:
result = subprocess.run(cmd, check=check, stderr=subprocess.DEVNULL,
stdout=subprocess.PIPE, text=True)
output = result.stdout
return output.splitlines()
except subprocess.CalledProcessError as err:
logger.error(f"{err}")
if default is not None:
return default
raise
def run_json_cmd(cmd, testfile, default=None, check=True):
"""Run a command (array of args) with JSON output and return the JSON"""
if TESTPATH and testfile:
cmd = ['cat', os.path.join(TESTPATH, testfile)]
try:
result = subprocess.run(cmd, check=check, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
output = result.stdout
data = json.loads(output)
except subprocess.CalledProcessError as err:
logger.error(f"{err}")
if default is not None:
return default
raise
except json.JSONDecodeError as err:
if check is True:
logger.error("failed parsing JSON output of command: "
f"{' '.join(cmd)}, error: {err}")
if default is not None:
return default
raise
return data
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_vpd_vendor_extensions(data):
vendor_extensions = []
for ext in data:
vendor_extension = {}
vendor_extension["iana-enterprise-number"] = ext[0]
vendor_extension["extension-data"] = ext[1]
vendor_extensions.append(vendor_extension)
return vendor_extensions
def get_vpd_data(vpd):
component = {}
component["name"] = vpd.get("board")
component["infix-hardware:vpd-data"] = {}
if vpd.get("data"):
component["class"] = "infix-hardware:vpd"
if vpd["data"].get("manufacture-date"):
component["mfg-date"] = datetime.strptime(vpd["data"]["manufacture-date"],"%m/%d/%Y %H:%M:%S").strftime("%Y-%m-%dT%H:%M:%SZ")
if vpd["data"].get("manufacter"):
component["mfg-name"] = vpd["data"]["manufacturer"]
if vpd["data"].get("product-name"):
component["model-name"] = vpd["data"]["product-name"]
if vpd["data"].get("serial-number"):
component["serial-num"] = vpd["data"]["serial-number"]
# Set VPD-data entrys
for k, v in vpd["data"].items():
if vpd["data"].get(k):
if k != "vendor-extension":
component["infix-hardware:vpd-data"][k] = v
else:
vendor_extensions=get_vpd_vendor_extensions(v)
component["infix-hardware:vpd-data"]["infix-hardware:vendor-extension"] = vendor_extensions
return component
def get_usb_ports(usb_ports):
ports=[]
names=[]
for usb_port in usb_ports:
port={}
if usb_port.get("path"):
if usb_port["name"] in names:
continue
path = usb_port["path"]
if os.path.basename(path) == "authorized_default":
if os.path.exists(path):
with open(path, "r") as f:
names.append(usb_port["name"])
data = int(f.readline().strip())
enabled = "unlocked" if data == 1 else "locked"
port["state"] = {}
port["state"]["admin-state"] = enabled
port["name"] = usb_port["name"]
port["class"] = "infix-hardware:usb"
port["state"]["oper-state"] = "enabled"
ports.append(port)
return ports
def add_hardware(hw_out):
data = run_json_cmd(['cat', "/run/system.json"], "system.json")
components = []
for _, vpd in data.get("vpd").items():
component = get_vpd_data(vpd)
components.append(component)
if data.get("usb-ports", None):
components.extend(get_usb_ports(data["usb-ports"]))
insert(hw_out, "component", components)
def uptime2datetime(uptime):
"""
Convert uptime to YANG format (YYYY-MM-DDTHH:MM:SS+00:00)
Handles the following input formats (frrtime):
HH:MM:SS
XdXXhXXm
XXwXdXXh
"""
h = m = s = 0
# Format HH:MM:SS
if match(r'^\d{2}:\d{2}:\d{2}$', uptime):
h, m, s = map(int, uptime.split(':'))
# Format XdXXhXXm (days, hours, minutes)
elif match(r'^\d+d\d{2}h\d{2}m$', uptime):
days = int(uptime.split('d')[0])
h = int(uptime.split('d')[1].split('h')[0])
m = int(uptime.split('h')[1].split('m')[0])
h += days * 24
# Format XwXdXXh (weeks, days, hours)
elif match(r'^\d{2}w\d{1}d\d{2}h$', uptime):
weeks = int(uptime.split('w')[0])
days = int(uptime.split('w')[1].split('d')[0])
h = int(uptime.split('d')[1].split('h')[0])
h += weeks * 7 * 24
h += days * 24
uptime_delta = timedelta(hours=h, minutes=m, seconds=s)
current_time = datetime_now()
last_updated = current_time - uptime_delta
date_timestd = last_updated.strftime('%Y-%m-%dT%H:%M:%S%z')
return date_timestd[:-2] + ':' + date_timestd[-2:]
def get_routes(routes, proto, data):
"""Populate routes from vtysh JSON output"""
# Mapping of FRR protocol names to IETF routing-protocol
pmap = {
'kernel': 'infix-routing:kernel',
'connected': 'direct',
'static': 'static',
'ospf': 'ietf-ospf:ospfv2',
'ospf6': 'ietf-ospf:ospfv3',
}
out = {}
out["route"] = []
if proto == "ipv4":
default = "0.0.0.0/0"
host_prefix_length = "32"
else:
default = "::/0"
host_prefix_length = "128"
for prefix, entries in data.items():
for route in entries:
new = {}
dst = route.get('prefix', default)
if '/' not in dst:
dst = f"{dst}/{route.get('prefixLen', host_prefix_length)}"
new[f'ietf-{proto}-unicast-routing:destination-prefix'] = dst
frr = route.get('protocol', 'infix-routing:kernel')
new['source-protocol'] = pmap.get(frr, 'infix-routing:kernel')
new['route-preference'] = route.get('distance', 0)
# Metric only available in the model for OSPF routes
if 'ospf' in frr:
new['ietf-ospf:metric'] = route.get('metric', 0)
# See https://datatracker.ietf.org/doc/html/rfc7951#section-6.9
# for details on how presence leaves are encoded in JSON: [null]
if route.get('selected', False):
new['active'] = [None]
new['last-updated'] = uptime2datetime(route.get('uptime', 0))
installed = route.get('installed', False)
next_hops = []
for hop in route.get('nexthops', []):
next_hop = {}
if hop.get('ip'):
next_hop[f'ietf-{proto}-unicast-routing:address'] = hop['ip']
elif hop.get('interfaceName'):
next_hop['outgoing-interface'] = hop['interfaceName']
# See zebra/zebra_vty.c:re_status_outpupt_char()
if installed and hop.get('fib', False):
next_hop['infix-routing:installed'] = [None]
next_hops.append(next_hop)
if next_hops:
new['next-hop'] = {'next-hop-list': {'next-hop': next_hops}}
else:
next_hop = {}
protocol = route.get('protocol', 'unicast')
if protocol == "blackhole":
next_hop['special-next-hop'] = "blackhole"
elif protocol == "unreachable":
next_hop['special-next-hop'] = "unreachable"
else:
if route.get('interfaceName'):
next_hop['outgoing-interface'] = route['interfaceName']
if route.get('nexthop'):
next_hop[f'ietf-{proto}-unicast-routing:next-hop-address'] = route['nexthop']
new['next-hop'] = next_hop
out['route'].append(new)
insert(routes, 'routes', out)
def add_ipv4_route(routes):
"""Fetch IPv4 routes from kernel and populate tree"""
data = run_json_cmd(['sudo', 'vtysh', '-c', "show ip route json"],
"vtysh-ip4-route.json", check=False, default={})
get_routes(routes, "ipv4", data)
def add_ipv6_route(routes):
"""Fetch IPv6 routes from kernel and populate tree"""
data = run_json_cmd(['sudo', 'vtysh', '-c', "show ipv6 route json"],
"vtysh-ip6-route.json", check=False, default={})
get_routes(routes, "ipv6", data)
def frr_to_ietf_neighbor_state(state):
"""Fetch OSPF neighbor state from Frr"""
state = state.split("/")[0]
if state == "TwoWay":
return "2-way"
return state.lower()
def add_ospf_routes(ospf):
"""Fetch OSPF routes from Frr"""
cmd = ['vtysh', '-c', "show ip ospf rout json"]
data = run_json_cmd(cmd, "", check=False, default=[])
if data == []:
return # No OSPF routes available
routes = []
for prefix, info in data.items():
if prefix.find("/") == -1: # Ignore router IDs
continue
route = {}
route["prefix"] = prefix
nexthops = []
routetype = info["routeType"].split(" ")
if len(routetype) > 1:
if routetype[1] == "E1":
route["route-type"] = "external-1"
elif routetype[1] == "E2":
route["route-type"] = "external-2"
elif routetype[1] == "IA":
route["route-type"] = "inter-area"
elif routetype[0] == "N":
route["route-type"] = "intra-area"
for hop in info["nexthops"]:
nexthop = {}
if hop["ip"] != " ":
nexthop["next-hop"] = hop["ip"]
else:
nexthop["outgoing-interface"] = hop["directlyAttachedTo"]
nexthops.append(nexthop)
route["next-hops"] = {}
route["next-hops"]["next-hop"] = nexthops
routes.append(route)
insert(ospf, "ietf-ospf:local-rib", "ietf-ospf:route", routes)
def add_ospf(control_protocols):
"""Populate OSPF status"""
cmd = ['/usr/libexec/statd/ospf-status']
data = run_json_cmd(cmd, "", check=False, default={})
if data == {}:
return # No OSPF data available
control_protocol = {}
control_protocol["type"] = "infix-routing:ospfv2"
control_protocol["name"] = "default"
control_protocol["ietf-ospf:ospf"] = {}
control_protocol["ietf-ospf:ospf"]["ietf-ospf:areas"] = {}
control_protocol["ietf-ospf:ospf"]["ietf-ospf:router-id"] = data.get("routerId")
control_protocol["ietf-ospf:ospf"]["ietf-ospf:address-family"] = "ipv4"
areas = []
for area_id, values in data.get("areas", {}).items():
area = {}
area["ietf-ospf:area-id"] = area_id
area["ietf-ospf:interfaces"] = {}
if values.get("area-type"):
area["ietf-ospf:area-type"] = values["area-type"]
interfaces = []
for iface in values.get("interfaces", {}):
interface = {}
interface["ietf-ospf:neighbors"] = {}
interface["name"] = iface["name"]
if iface.get("drId"):
interface["dr-router-id"] = iface["drId"]
if iface.get("drAddress"):
interface["dr-ip-addr"] = iface["drAddress"]
if iface.get("bdrId"):
interface["bdr-router-id"] = iface["bdrId"]
if iface.get("bdrAddress"):
interface["bdr-ip-addr"] = iface["bdrAddress"]
if iface.get("timerPassiveIface"):
interface["passive"] = True
else:
interface["passive"] = False
interface["enabled"] = iface["ospfEnabled"]
if iface["networkType"] == "POINTOPOINT":
interface["interface-type"] = "point-to-point"
if iface["networkType"] == "BROADCAST":
interface["interface-type"] = "broadcast"
if iface.get("state"):
# Wev've never seen "DependUpon", and has no entry in
# the YANG model, but is listed before down in Frr
xlate = {
"DependUpon": "down",
"Down": "down",
"Waiting": "waiting",
"Loopback": "loopback",
"Point-To-Point": "point-to-point",
"DROther": "dr-other",
"Backup": "bdr",
"DR": "dr"
}
val = xlate.get(iface["state"], "unknown")
interface["state"] = val
neighbors = []
for neigh in iface["neighbors"]:
neighbor = {}
neighbor["neighbor-router-id"] = neigh["neighborIp"]
neighbor["address"] = neigh["ifaceAddress"]
neighbor["dead-timer"] = neigh["routerDeadIntervalTimerDueMsec"]
neighbor["state"] = frr_to_ietf_neighbor_state(neigh["nbrState"])
if neigh.get("routerDesignatedId"):
neighbor["dr-router-id"] = neigh["routerDesignatedId"]
if neigh.get("routerDesignatedBackupId"):
neighbor["bdr-router-id"] = neigh["routerDesignatedBackupId"]
neighbors.append(neighbor)
interface["ietf-ospf:neighbors"] = {}
interface["ietf-ospf:neighbors"]["ietf-ospf:neighbor"] = neighbors
interfaces.append(interface)
area["ietf-ospf:interfaces"]["ietf-ospf:interface"] = interfaces
areas.append(area)
add_ospf_routes(control_protocol["ietf-ospf:ospf"])
control_protocol["ietf-ospf:ospf"]["ietf-ospf:areas"]["ietf-ospf:area"] = areas
insert(control_protocols, "control-plane-protocol", [control_protocol])
def get_bridge_port_pvid(ifname):
data = run_json_cmd(['bridge', '-j', 'vlan', 'show', 'dev', ifname],
f"bridge-vlan-show-dev-{ifname}.json")
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 = run_json_cmd(['bridge', '-j', 'link', 'show', 'dev', ifname],
f"bridge-link-show-dev-{ifname}.json")
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 container_inspect(name):
"""Call podman inspect {name}, return object at {path} or None."""
cmd = ['podman', 'inspect', name]
try:
return run_json_cmd(cmd, "", default=[])
except Exception as e:
logging.error(f"Error running podman inspect: {e}")
return []
def add_container(containers):
"""We list *all* containers, not just those in the configuraion."""
cmd = ['podman', 'ps', '-a', '--format=json']
raw = run_json_cmd(cmd, "", default=[])
for entry in raw:
running = entry["State"] == "running"
container = {
"name": entry["Names"][0],
"id": entry["Id"],
"image": entry["Image"],
"image-id": entry["ImageID"],
"running": running,
"status": entry["Status"]
}
# Bonus information, may not be available
if entry["Command"]:
container["command"] = " ".join(entry["Command"])
# The 'podman ps' command lists ports even in host mode, but
# that's not applicable, so skip networks and port forwardings
cont = container_inspect(container["name"])
if cont and isinstance(cont, list) and len(cont) > 0:
cont = cont[0]
else:
cont = {}
networks = cont.get("NetworkSettings", {}).get("Networks")
if networks and "host" in networks:
container["network"] = {"host": True}
else:
container["network"] = {
"interface": [],
"publish": []
}
if entry["Networks"]:
for net in entry["Networks"]:
container["network"]["interface"].append({"name": net})
if running and entry["Ports"]:
for port in entry["Ports"]:
addr = ""
if port["host_ip"]:
addr = f"{port['host_ip']}:"
pub = f"{addr}{port['host_port']}->{port['container_port']}/{port['protocol']}"
container["network"]["publish"].append(pub)
containers.append(container)
def get_brport_multicast(ifname):
"""Check if multicast snooping is enabled on bridge, default: nope"""
data = run_json_cmd(['mctl', 'show', 'igmp', 'json'], "igmp-status.json",
default={}, check=False)
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 run_json_cmd(['ip', '-s', '-d', '-j', 'link', 'show'],
"ip-link-show.json")
def netns_get_ip_link(netns):
"""Fetch interface link information from within a network namespace"""
return run_json_cmd(['ip', 'netns', 'exec', netns, 'ip', '-s', '-d', '-j', 'link', 'show'],
f"netns-{netns}-ip-link-show.json")
def get_ip_addr():
"""Fetch interface address information from kernel"""
return run_json_cmd(['ip', '-j', 'addr', 'show'],
"ip-addr-show.json")
def netns_get_ip_addr(netns):
"""Fetch interface address information from within a network namespace"""
return run_json_cmd(['ip', 'netns', 'exec', netns, 'ip', '-j', 'addr', 'show'],
f"netns-{netns}-ip-addr-show.json")
def get_netns_list():
"""Fetch a list of network namespaces"""
return run_json_cmd(['ip', '-j', 'netns', 'list'],
"netns-list.json")
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_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:
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 'link' in iface_in:
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
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'])
# We avoid importing os to check if the file exists (for performance)
val = get_proc_value(f"/proc/sys/net/ipv6/conf/{ifname}/mtu")
if val is not None:
insert(iface_out, "ietf-ip:ipv6", "mtu", int(val))
if 'addr_info' in iface_in:
inet = []
inet6 = []
for addr in iface_in['addr_info']:
new = {}
if 'family' not in addr:
logger.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:
logger.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 = run_json_cmd(cmd, f"ethtool-groups-{ifname}.json")
if len(data) != 1:
logger.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 = run_cmd(['ethtool', ifname], f"ethtool-{ifname}.txt")
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 = run_json_cmd(['podman', 'ps', '--format', 'json'], "podman-ps.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 run_json_cmd(['bridge', '-j', 'link'], "bridge-link.json"):
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 run_json_cmd(['bridge', '-j', 'vlan'], "bridge-vlan.json"):
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 = run_json_cmd(['mctl', '-p', 'show', 'igmp', 'json'],
"igmp-status.json", default={},
check=False)
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))
def add_interface(ifname, yang_ifaces):
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, yang_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, yang_ifaces)
add_container_ifaces(yang_ifaces)
def main():
global TESTPATH
global logger
parser = argparse.ArgumentParser(description="YANG data creator")
parser.add_argument("model", help="YANG Model")
parser.add_argument("-p", "--param", default=None, help="Model dependent parameter")
parser.add_argument("-t", "--test", default=None, help="Test data base path")
args = parser.parse_args()
if args.test:
TESTPATH = args.test
else:
TESTPATH = ""
# Set up syslog output for critical errors to aid debugging
logger = logging.getLogger('yanger')
if os.path.exists('/dev/log'):
log = logging.handlers.SysLogHandler(address='/dev/log')
else:
# Use /dev/null as a fallback for unit tests
log = logging.FileHandler('/dev/null')
fmt = logging.Formatter('%(name)s[%(process)d]: %(message)s')
log.setFormatter(fmt)
logger.setLevel(logging.INFO)
logger.addHandler(log)
if args.model == 'ietf-interfaces':
yang_data = {
"ietf-interfaces:interfaces": {
"interface": []
}
}
add_interface(args.param, yang_data['ietf-interfaces:interfaces']['interface'])
elif args.model == 'ietf-routing':
yang_data = {
"ietf-routing:routing": {
"ribs": {
"rib": [{
"name": "ipv4",
"address-family": "ipv4"
}, {
"name": "ipv6",
"address-family": "ipv6"
}]
}
}
}
ipv4routes = yang_data['ietf-routing:routing']['ribs']['rib'][0]
ipv6routes = yang_data['ietf-routing:routing']['ribs']['rib'][1]
add_ipv4_route(ipv4routes)
add_ipv6_route(ipv6routes)
elif args.model == 'ietf-ospf':
yang_data = {
"ietf-routing:routing": {
"control-plane-protocols": {
}
}
}
add_ospf(yang_data['ietf-routing:routing']['control-plane-protocols'])
elif args.model == 'ietf-hardware':
yang_data = {
"ietf-hardware:hardware": {
}
}
add_hardware(yang_data["ietf-hardware:hardware"])
elif args.model == 'infix-containers':
yang_data = {
"infix-containers:containers": {
"container": []
}
}
add_container(yang_data['infix-containers:containers']['container'])
else:
logger.warning(f"Unsupported model {args.model}", file=sys.stderr)
sys.exit(1)
print(json.dumps(yang_data, indent=2))
if __name__ == "__main__":
main()