mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 21:13:00 +02:00
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>
253 lines
9.8 KiB
Python
253 lines
9.8 KiB
Python
from .common import insert
|
|
from .host import HOST
|
|
|
|
|
|
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 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"]
|
|
data = HOST.run_json(cmd, 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"
|
|
|
|
# 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"] != " ":
|
|
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_areas(control_protocols):
|
|
"""Populate OSPF status"""
|
|
cmd = ['/usr/libexec/statd/ospf-status']
|
|
data = HOST.run_json(cmd, 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
|
|
|
|
# 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"]
|
|
|
|
# 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"):
|
|
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_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 operational():
|
|
out = {
|
|
"ietf-routing:routing": {
|
|
"control-plane-protocols": {
|
|
}
|
|
}
|
|
}
|
|
|
|
add_areas(out['ietf-routing:routing']['control-plane-protocols'])
|
|
return out
|