statd: extend ospf operational data to replace vtysh commands in cli

Collect more data from FRR to the OSPF and BFD operational datastore.
Refactor CLI commands to use the YANG datastore instead of vtysh.

Additionally, the BFD command(s) have been moved to the top level of
admin-exec, which is where vtysh has them.

Finally, preparing for future OSPFv3 support, all commands have been
given a new context under 'show ip ospf'.  Deprecation warnings have
been added to the existing 'show ospf' commands.

Also, for consistency, all commands names in plural have been changed to
their singular form: routes -> route, interfaes -> interface, etc.  This
is what vtysh, Cisco, and others do as well.

Fixes #1190

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-11-12 22:28:18 +01:00
parent b1f83e5ce7
commit f79c0ee903
12 changed files with 1484 additions and 46 deletions
+3
View File
@@ -81,6 +81,9 @@ def main():
elif args.model == 'infix-firewall':
from . import infix_firewall
yang_data = infix_firewall.operational()
elif args.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)
sys.exit(1)
+96
View File
@@ -0,0 +1,96 @@
from .common import insert
from .host import HOST
def frr_to_ietf_state(state):
"""Convert FRR BFD state to IETF BFD state"""
state_map = {
"up": "up",
"down": "down",
"init": "init",
"adminDown": "adminDown"
}
return state_map.get(state, "down")
def add_sessions(control_protocols):
"""Fetch BFD session data from FRR"""
cmd = ['vtysh', '-c', 'show bfd peers json']
data = HOST.run_json(cmd, default=[])
if not data:
return # No BFD sessions available
control_protocol = {}
control_protocol["type"] = "infix-routing:bfdv1"
control_protocol["name"] = "bfd"
control_protocol["ietf-bfd:bfd"] = {}
control_protocol["ietf-bfd:bfd"]["ietf-bfd-ip-sh:ip-sh"] = {}
control_protocol["ietf-bfd:bfd"]["ietf-bfd-ip-sh:ip-sh"]["sessions"] = {}
sessions = []
# FRR returns a list of BFD peers
for peer in data:
# Only process single-hop sessions (multihop == false)
if peer.get("multihop", False):
continue
session = {}
# Key fields: interface and dest-addr
session["interface"] = peer.get("interface", "unknown")
session["dest-addr"] = peer.get("peer", "0.0.0.0")
# Operational state fields (config false)
# Local and remote discriminators
if peer.get("id") is not None:
session["local-discriminator"] = peer["id"]
if peer.get("remote-id") is not None:
session["remote-discriminator"] = peer["remote-id"]
# Session running state
session["session-running"] = {}
# Local and remote state
state = peer.get("status", "down")
session["session-running"]["local-state"] = frr_to_ietf_state(state)
# Remote state not directly available in FRR output, infer from status
session["session-running"]["remote-state"] = frr_to_ietf_state(state)
# Local diagnostic - not directly available, use "none"
session["session-running"]["local-diagnostic"] = "none"
# Detection mode - FRR uses async mode for OSPF-created sessions
session["session-running"]["detection-mode"] = "async-without-echo"
# Timing intervals (convert milliseconds to microseconds for YANG)
if peer.get("receive-interval") is not None:
session["session-running"]["negotiated-rx-interval"] = peer["receive-interval"] * 1000
if peer.get("transmit-interval") is not None:
session["session-running"]["negotiated-tx-interval"] = peer["transmit-interval"] * 1000
# Detection time (in microseconds)
if peer.get("detect-multiplier") is not None and peer.get("receive-interval") is not None:
detection_time_ms = peer["detect-multiplier"] * peer["receive-interval"]
session["session-running"]["detection-time"] = detection_time_ms * 1000
# Path type
session["path-type"] = "ietf-bfd-types:path-ip-sh"
session["ip-encapsulation"] = True
sessions.append(session)
if sessions:
control_protocol["ietf-bfd:bfd"]["ietf-bfd-ip-sh:ip-sh"]["sessions"]["session"] = sessions
insert(control_protocols, "control-plane-protocol", [control_protocol])
def operational():
out = {
"ietf-routing:routing": {
"control-plane-protocols": {}
}
}
add_sessions(out['ietf-routing:routing']['control-plane-protocols'])
return out
+97 -1
View File
@@ -10,6 +10,14 @@ def frr_to_ietf_neighbor_state(state):
return state.lower()
def frr_to_ietf_neighbor_role(role):
"""Translate FRR neighbor role to YANG enumeration values"""
if role == "Backup":
return "BDR"
# DR and DROther are already correct
return role
def add_routes(ospf):
"""Fetch OSPF routes from Frr"""
cmd = ['vtysh', '-c', "show ip ospf route json"]
@@ -38,6 +46,22 @@ def add_routes(ospf):
elif routetype[0] == "N":
route["route-type"] = "intra-area"
# Add area information if available
# Note: augmented by infix-routing.yang since standard ietf-ospf doesn't include it
# Must use the augmenting module's prefix
if info.get("area") is not None:
route["infix-routing:area-id"] = info["area"]
# Add metric (cost) if available
if info.get("cost") is not None:
route["metric"] = info["cost"]
elif info.get("metric") is not None:
route["metric"] = info["metric"]
# Add route-tag for external routes
if info.get("tag") is not None:
route["route-tag"] = info["tag"]
for hop in info["nexthops"]:
nexthop = {}
if hop["ip"] != " ":
@@ -119,13 +143,85 @@ def add_areas(control_protocols):
val = xlate.get(iface["state"], "unknown")
interface["state"] = val
# Interface priority (for DR/BDR election)
if iface.get("priority") is not None:
interface["priority"] = iface["priority"]
# Interface cost
if iface.get("cost") is not None:
interface["cost"] = iface["cost"]
# Configuration timers (in seconds)
if iface.get("timerDeadSecs") is not None:
interface["dead-interval"] = iface["timerDeadSecs"]
if iface.get("timerRetransmitSecs") is not None:
interface["retransmit-interval"] = iface["timerRetransmitSecs"]
if iface.get("transmitDelaySecs") is not None:
interface["transmit-delay"] = iface["transmitDelaySecs"]
# Hello interval - convert from milliseconds to seconds
if iface.get("timerMsecs") is not None:
hello_sec = iface["timerMsecs"] // 1000
# timer-value-seconds16 requires range 1..65535, use max(1, value)
if hello_sec >= 1:
interface["hello-interval"] = hello_sec
# Operational state timers (config false)
# Hello timer - time remaining until next Hello (convert ms to seconds)
if iface.get("timerHelloInMsecs") is not None:
hello_timer_sec = iface["timerHelloInMsecs"] // 1000
# timer-value-seconds16 requires range 1..65535, use max(1, value)
if hello_timer_sec >= 1:
interface["hello-timer"] = hello_timer_sec
# Wait timer - time until interface exits Waiting state
if iface.get("timerWaitSecs") is not None:
wait_sec = iface["timerWaitSecs"]
# timer-value-seconds16 requires range 1..65535
if wait_sec >= 1:
interface["wait-timer"] = wait_sec
neighbors = []
for neigh in iface["neighbors"]:
neighbor = {}
neighbor["neighbor-router-id"] = neigh["neighborIp"]
neighbor["address"] = neigh["ifaceAddress"]
neighbor["dead-timer"] = neigh["routerDeadIntervalTimerDueMsec"]
# Priority - use existing YANG leaf for operational data
if neigh.get("nbrPriority") is not None:
neighbor["priority"] = neigh["nbrPriority"]
# Uptime - convert from milliseconds to seconds
# Note: augmented by infix-routing.yang
# Use lastPrgrsvChangeMsec from detail output (time since last progressive state change)
if neigh.get("lastPrgrsvChangeMsec") is not None:
uptime_sec = neigh["lastPrgrsvChangeMsec"] // 1000
neighbor["infix-routing:uptime"] = uptime_sec
# Dead timer - convert from milliseconds to seconds
# timer-value-seconds16 requires range 1..65535
if neigh.get("routerDeadIntervalTimerDueMsec") is not None:
dead_timer_sec = neigh["routerDeadIntervalTimerDueMsec"] // 1000
if dead_timer_sec >= 1:
neighbor["dead-timer"] = dead_timer_sec
neighbor["state"] = frr_to_ietf_neighbor_state(neigh["nbrState"])
# Store role (DR/BDR/DROther) for display
# Note: augmented by infix-routing.yang
if neigh.get("role"):
neighbor["infix-routing:role"] = frr_to_ietf_neighbor_role(neigh["role"])
# Store interface name with local address (e.g., "e5:10.0.23.1")
# Note: augmented by infix-routing.yang
# Compose from ifaceName and localIfaceAddress
if neigh.get("ifaceName") and neigh.get("localIfaceAddress"):
neighbor["infix-routing:interface-name"] = f"{neigh['ifaceName']}:{neigh['localIfaceAddress']}"
elif neigh.get("ifaceName"):
neighbor["infix-routing:interface-name"] = neigh["ifaceName"]
if neigh.get("routerDesignatedId"):
neighbor["dr-router-id"] = neigh["routerDesignatedId"]
if neigh.get("routerDesignatedBackupId"):