statd: optimize yanger invocation time and reduce command overhead

Replace logging + logging.handlers with a lightweight syslog wrapper,
and argparse with manual argv parsing.  On a sama7g54, this cuts yanger
startup from ~770ms to ~470ms by eliminating ~300ms of stdlib imports.

Also batch external command invocations:

 - ietf_routing: two sysctl calls instead of two per interface
 - ietf_hardware: one ls per hwmon device instead of six
 - bridge: fetch mctl querier data once instead of once per VLAN

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-03-20 16:18:10 +01:00
parent 5583b97da9
commit d3c091e746
5 changed files with 178 additions and 80 deletions
+92 -59
View File
@@ -1,97 +1,130 @@
import logging
import logging.handlers
import json
import sys # (built-in module)
import os
import argparse
import sys
from . import common
from . import host
USAGE = """\
usage: yanger [-p PARAM] [-x PREFIX] [-r DIR | -c DIR] model
YANG data creator
positional arguments:
model YANG Model
options:
-p, --param PARAM Model dependent parameter, e.g. interface name
-x, --cmd-prefix PREFIX
Use this prefix for all system commands, e.g.
'ssh user@remotehost sudo'
-r, --replay DIR Generate output based on recorded system commands
from DIR, rather than querying the local system
-c, --capture DIR Capture system command output in DIR, such that the
current system state can be recreated offline (with
--replay) for testing purposes
"""
def _parse_args(argv):
model = None
param = None
cmd_prefix = None
replay = None
capture = None
i = 1
while i < len(argv):
arg = argv[i]
if arg in ('-h', '--help'):
sys.stdout.write(USAGE)
sys.exit(0)
elif arg in ('-p', '--param'):
i += 1
if i >= len(argv):
sys.exit(f"error: {arg} requires an argument")
param = argv[i]
elif arg in ('-x', '--cmd-prefix'):
i += 1
if i >= len(argv):
sys.exit(f"error: {arg} requires an argument")
cmd_prefix = argv[i]
elif arg in ('-r', '--replay'):
i += 1
if i >= len(argv):
sys.exit(f"error: {arg} requires an argument")
replay = argv[i]
if not os.path.isdir(replay):
sys.exit(f"error: '{replay}' is not a valid directory")
elif arg in ('-c', '--capture'):
i += 1
if i >= len(argv):
sys.exit(f"error: {arg} requires an argument")
capture = argv[i]
elif arg.startswith('-'):
sys.exit(f"error: unknown option: {arg}")
elif model is None:
model = arg
else:
sys.exit(f"error: unexpected argument: {arg}")
i += 1
if model is None:
sys.exit("error: missing required argument: model")
if replay and cmd_prefix:
sys.exit("error: --cmd-prefix cannot be used with --replay")
if replay and capture:
sys.exit("error: --replay cannot be used with --capture")
return model, param, cmd_prefix, replay, capture
def main():
def dirpath(path):
if not os.path.isdir(path):
raise argparse.ArgumentTypeError(f"'{path}' is not a valid directory")
return path
model, param, cmd_prefix, replay, capture = _parse_args(sys.argv)
parser = argparse.ArgumentParser(description="YANG data creator")
parser.add_argument("model", help="YANG Model")
parser.add_argument("-p", "--param",
help="Model dependent parameter, e.g. interface name")
parser.add_argument("-x", "--cmd-prefix", metavar="PREFIX",
help="Use this prefix for all system commands, e.g. " +
"'ssh user@remotehost sudo'")
rrparser = parser.add_mutually_exclusive_group()
rrparser.add_argument("-r", "--replay", type=dirpath, metavar="DIR",
help="Generate output based on recorded system commands from DIR, " +
"rather than querying the local system")
rrparser.add_argument("-c", "--capture", metavar="DIR",
help="Capture system command output in DIR, such that the current system " +
"state can be recreated offline (with --replay) for testing purposes")
args = parser.parse_args()
if args.replay and args.cmd_prefix:
parser.error("--cmd-prefix cannot be used with --replay")
# Set up syslog output for critical errors to aid debugging
common.LOG = 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)
common.LOG.setLevel(logging.INFO)
common.LOG.addHandler(log)
if args.cmd_prefix or args.capture:
host.HOST = host.Remotehost(args.cmd_prefix, args.capture)
elif args.replay:
host.HOST = host.Replayhost(args.replay)
if cmd_prefix or capture:
host.HOST = host.Remotehost(cmd_prefix, capture)
elif replay:
host.HOST = host.Replayhost(replay)
else:
host.HOST = host.Localhost()
if args.model == 'ietf-interfaces':
if model == 'ietf-interfaces':
from . import ietf_interfaces
yang_data = ietf_interfaces.operational(args.param)
elif args.model == 'ietf-routing':
yang_data = ietf_interfaces.operational(param)
elif model == 'ietf-routing':
from . import ietf_routing
yang_data = ietf_routing.operational()
elif args.model == 'ietf-ospf':
elif model == 'ietf-ospf':
from . import ietf_ospf
yang_data = ietf_ospf.operational()
elif args.model == 'ietf-rip':
elif model == 'ietf-rip':
from . import ietf_rip
yang_data = ietf_rip.operational()
elif args.model == 'ietf-hardware':
elif model == 'ietf-hardware':
from . import ietf_hardware
yang_data = ietf_hardware.operational()
elif args.model == 'infix-containers':
elif model == 'infix-containers':
from . import infix_containers
yang_data = infix_containers.operational()
elif args.model == 'infix-dhcp-server':
elif model == 'infix-dhcp-server':
from . import infix_dhcp_server
yang_data = infix_dhcp_server.operational()
elif args.model == 'ietf-system':
elif model == 'ietf-system':
from . import ietf_system
yang_data = ietf_system.operational()
elif args.model == 'ietf-ntp':
elif model == 'ietf-ntp':
from . import ietf_ntp
yang_data = ietf_ntp.operational()
elif args.model == 'ieee802-dot1ab-lldp':
elif model == 'ieee802-dot1ab-lldp':
from . import infix_lldp
yang_data = infix_lldp.operational()
elif args.model == 'infix-firewall':
elif model == 'infix-firewall':
from . import infix_firewall
yang_data = infix_firewall.operational()
elif args.model == 'ietf-bfd-ip-sh':
elif model == 'ietf-bfd-ip-sh':
from . import ietf_bfd_ip_sh
yang_data = ietf_bfd_ip_sh.operational()
else:
common.LOG.warning("Unsupported model %s", args.model)
common.LOG.warning("Unsupported model %s", model)
sys.exit(1)
print(json.dumps(yang_data, indent=2, ensure_ascii=False))
+43 -1
View File
@@ -1,8 +1,50 @@
import syslog
from datetime import timedelta
from . import host
LOG = None
class SysLog:
"""Lightweight syslog wrapper replacing the logging module.
Provides the same .error()/.warning()/.info()/.debug() interface
used throughout yanger, but uses the C syslog facility directly,
avoiding the ~374ms import overhead of logging + logging.handlers.
"""
DEBUG = syslog.LOG_DEBUG
INFO = syslog.LOG_INFO
WARNING = syslog.LOG_WARNING
ERROR = syslog.LOG_ERR
def __init__(self, name):
syslog.openlog(name, syslog.LOG_PID)
self._level = self.INFO
def setLevel(self, level):
self._level = level
def _log(self, level, msg, *args):
if level > self._level:
return
if args:
msg = msg % args
syslog.syslog(level, msg)
def debug(self, msg, *args):
self._log(self.DEBUG, msg, *args)
def info(self, msg, *args):
self._log(self.INFO, msg, *args)
def warning(self, msg, *args):
self._log(self.WARNING, msg, *args)
def error(self, msg, *args):
self._log(self.ERROR, msg, *args)
LOG = SysLog("yanger")
class YangDate:
def __init__(self, dt=None):
+9 -6
View File
@@ -265,8 +265,11 @@ def hwmon_sensor_components():
component["description"] = desc
return component
# List hwmon directory once, reuse for all sensor types
all_entries = HOST.run(("ls", hwmon_path), default="").split()
# Temperature sensors
temp_entries = HOST.run(("ls", hwmon_path), default="").split()
temp_entries = all_entries
temp_files = [os.path.join(hwmon_path, e) for e in temp_entries if e.startswith("temp") and e.endswith("_input")]
for temp_file in temp_files:
try:
@@ -285,7 +288,7 @@ def hwmon_sensor_components():
continue
# Fan sensors (RPM from tachometer)
fan_entries = HOST.run(("ls", hwmon_path), default="").split()
fan_entries = all_entries
fan_files = [os.path.join(hwmon_path, e) for e in fan_entries if e.startswith("fan") and e.endswith("_input")]
for fan_file in fan_files:
try:
@@ -307,7 +310,7 @@ def hwmon_sensor_components():
# Only add if no fan*_input exists for this device (avoid duplicates)
has_rpm_sensor = bool(fan_files)
if not has_rpm_sensor:
pwm_entries = HOST.run(("ls", hwmon_path), default="").split()
pwm_entries = all_entries
pwm_files = [os.path.join(hwmon_path, e) for e in pwm_entries if e.startswith("pwm") and e[3:].replace('_', '').isdigit() if len(e) > 3]
for pwm_file in pwm_files:
# Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc.
@@ -336,7 +339,7 @@ def hwmon_sensor_components():
continue
# Voltage sensors
voltage_entries = HOST.run(("ls", hwmon_path), default="").split()
voltage_entries = all_entries
voltage_files = [os.path.join(hwmon_path, e) for e in voltage_entries if e.startswith("in") and e.endswith("_input")]
for voltage_file in voltage_files:
try:
@@ -356,7 +359,7 @@ def hwmon_sensor_components():
continue
# Current sensors
current_entries = HOST.run(("ls", hwmon_path), default="").split()
current_entries = all_entries
current_files = [os.path.join(hwmon_path, e) for e in current_entries if e.startswith("curr") and e.endswith("_input")]
for current_file in current_files:
try:
@@ -376,7 +379,7 @@ def hwmon_sensor_components():
continue
# Power sensors
power_entries = HOST.run(("ls", hwmon_path), default="").split()
power_entries = all_entries
power_files = [os.path.join(hwmon_path, e) for e in power_entries if e.startswith("power") and e.endswith("_input")]
for power_file in power_files:
try:
@@ -204,10 +204,13 @@ def mctlq2yang_mode(mctlq):
return "off"
def mctl(ifname, vid):
mctl = HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={})
def mctl_queriers():
"""Fetch all IGMP multicast querier data in one call"""
return HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={})
for q in mctl.get("multicast-queriers", []):
def mctl(ifname, vid, mctldata):
for q in mctldata.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
@@ -239,8 +242,8 @@ def multicast_filters(iplink, vid):
return { "multicast-filter": list(mdb.values()) }
def multicast(iplink, info):
mctlq = mctl(iplink["ifname"], info.get("vlan"))
def multicast(iplink, info, mctldata):
mctlq = mctl(iplink["ifname"], info.get("vlan"), mctldata)
mcast = {
"snooping": bool(info.get("mcast_snooping")),
@@ -276,13 +279,15 @@ def vlans(iplink):
if not (brgvlans := HOST.run_json(f"bridge -j vlan global show dev {iplink['ifname']}".split())):
return []
mctldata = mctl_queriers()
vlans = {
v["vlan"]: {
"vid": v["vlan"],
"untagged": [],
"tagged": [],
"multicast": multicast(iplink, v),
"multicast": multicast(iplink, v, mctldata),
"multicast-filters": multicast_filters(iplink, v["vlan"]),
}
for v in brgvlans[0]["vlans"]
@@ -307,7 +312,7 @@ def dbridge(iplink):
info = iplink["linkinfo"]["info_data"]
return {
"multicast": multicast(iplink, info),
"multicast": multicast(iplink, info, mctl_queriers()),
"multicast-filters": multicast_filters(iplink, None),
}
+22 -7
View File
@@ -132,19 +132,34 @@ def get_routing_interfaces():
links_json = HOST.run(tuple(['ip', '-j', 'link', 'show']), default="[]")
links = json.loads(links_json)
# Fetch all forwarding sysctls in two calls instead of 2 per interface
ipv4_sysctls = HOST.run(tuple(['sysctl', 'net.ipv4.conf']), default="")
ipv6_sysctls = HOST.run(tuple(['sysctl', 'net.ipv6.conf']), default="")
# Parse "net.ipv4.conf.<iface>.forwarding = 1" lines into a set
ipv4_fwd = set()
ipv6_fwd = set()
for line in ipv4_sysctls.splitlines():
if '.forwarding = 1' in line:
# net.ipv4.conf.IFNAME.forwarding = 1
parts = line.split('.')
if len(parts) >= 5:
ipv4_fwd.add(parts[3])
for line in ipv6_sysctls.splitlines():
if '.force_forwarding = 1' in line:
# net.ipv6.conf.IFNAME.force_forwarding = 1
parts = line.split('.')
if len(parts) >= 5:
ipv6_fwd.add(parts[3])
routing_ifaces = []
for link in links:
ifname = link.get('ifname')
if not ifname:
continue
# Check if IPv4 forwarding is enabled
ipv4_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv4.conf.{ifname}.forwarding']), default="0").strip()
# Check if IPv6 force_forwarding is enabled (available since Linux 6.17)
ipv6_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv6.conf.{ifname}.force_forwarding']), default="0").strip()
if ipv4_fwd == "1" or ipv6_fwd == "1":
if ifname in ipv4_fwd or ifname in ipv6_fwd:
routing_ifaces.append(ifname)
return routing_ifaces