mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-02 22:03:01 +02:00
confd: initial support for rip
Fixes #582 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -3260,6 +3260,165 @@ def show_ospf_routes(json_data):
|
||||
print(f"{'':<52} {next_hop_addr:<16} {outgoing_iface:<12}")
|
||||
|
||||
|
||||
def show_rip(json_data):
|
||||
"""Show RIP general instance information"""
|
||||
routing = json_data.get('ietf-routing:routing', {})
|
||||
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
|
||||
|
||||
rip_instance = None
|
||||
for protocol in protocols:
|
||||
if 'ietf-rip:rip' in protocol:
|
||||
rip_instance = protocol
|
||||
break
|
||||
|
||||
if not rip_instance:
|
||||
print("RIP is not configured or running")
|
||||
return
|
||||
|
||||
rip = rip_instance.get('ietf-rip:rip', {})
|
||||
|
||||
# Display RIP configuration
|
||||
print(" RIP Routing Process")
|
||||
print()
|
||||
|
||||
distance = rip.get('distance', 120)
|
||||
default_metric = rip.get('default-metric', 1)
|
||||
num_routes = rip.get('num-of-routes', 0)
|
||||
|
||||
print(f" Administrative distance: {distance}")
|
||||
print(f" Default metric: {default_metric}")
|
||||
print(f" Number of RIP routes: {num_routes}")
|
||||
print()
|
||||
|
||||
# Display timers if available
|
||||
timers = rip.get('timers', {})
|
||||
if timers:
|
||||
update = timers.get('update-interval', 30)
|
||||
invalid = timers.get('invalid-interval', 180)
|
||||
flush = timers.get('flush-interval', 240)
|
||||
print(" Timers:")
|
||||
print(f" Update interval: {update} seconds")
|
||||
print(f" Invalid interval: {invalid} seconds")
|
||||
print(f" Flush interval: {flush} seconds")
|
||||
print()
|
||||
|
||||
# Display interfaces
|
||||
interfaces = rip.get('interfaces', {}).get('interface', [])
|
||||
if interfaces:
|
||||
print(f" Number of interfaces: {len(interfaces)}")
|
||||
for iface in interfaces:
|
||||
iface_name = iface.get('interface', 'unknown')
|
||||
print(f" {iface_name}")
|
||||
print()
|
||||
|
||||
|
||||
def show_rip_routes(json_data):
|
||||
"""Show RIP routing table"""
|
||||
routing = json_data.get('ietf-routing:routing', {})
|
||||
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
|
||||
|
||||
rip_instance = None
|
||||
for protocol in protocols:
|
||||
if 'ietf-rip:rip' in protocol:
|
||||
rip_instance = protocol
|
||||
break
|
||||
|
||||
if not rip_instance:
|
||||
print("RIP is not configured or running")
|
||||
return
|
||||
|
||||
rip = rip_instance.get('ietf-rip:rip', {})
|
||||
routes = rip.get('ipv4', {}).get('routes', {}).get('route', [])
|
||||
|
||||
if not routes:
|
||||
print("No RIP routes")
|
||||
return
|
||||
|
||||
# Header
|
||||
hdr = f"{'PREFIX':<20} {'METRIC':<8} {'NEXT-HOP':<16} {'INTERFACE':<12}"
|
||||
print(Decore.invert(hdr))
|
||||
|
||||
for route in routes:
|
||||
prefix = route.get('ipv4-prefix', 'unknown')
|
||||
metric = route.get('metric', 0)
|
||||
next_hop = route.get('next-hop', '-')
|
||||
interface = route.get('interface', '-')
|
||||
|
||||
print(f"{prefix:<20} {metric:<8} {next_hop:<16} {interface:<12}")
|
||||
|
||||
|
||||
def show_rip_interfaces(json_data):
|
||||
"""Show RIP interface information"""
|
||||
routing = json_data.get('ietf-routing:routing', {})
|
||||
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
|
||||
|
||||
rip_instance = None
|
||||
for protocol in protocols:
|
||||
if 'ietf-rip:rip' in protocol:
|
||||
rip_instance = protocol
|
||||
break
|
||||
|
||||
if not rip_instance:
|
||||
print("RIP is not configured or running")
|
||||
return
|
||||
|
||||
rip = rip_instance.get('ietf-rip:rip', {})
|
||||
interfaces = rip.get('interfaces', {}).get('interface', [])
|
||||
|
||||
if not interfaces:
|
||||
print("No RIP interfaces")
|
||||
return
|
||||
|
||||
# Header
|
||||
hdr = f"{'INTERFACE':<12} {'STATUS':<10} {'SEND':<6} {'RECV':<6} {'SPLIT-HORIZON':<20} {'PASSIVE':<10}"
|
||||
print(Decore.invert(hdr))
|
||||
|
||||
for iface in interfaces:
|
||||
iface_name = iface.get('interface', 'unknown')
|
||||
oper_status = iface.get('oper-status', 'down')
|
||||
send_ver = iface.get('send-version', '-')
|
||||
recv_ver = iface.get('receive-version', '-')
|
||||
split_horizon = iface.get('split-horizon', 'simple')
|
||||
passive = 'Yes' if iface.get('passive', False) else 'No'
|
||||
|
||||
print(f"{iface_name:<12} {oper_status:<10} {send_ver:<6} {recv_ver:<6} {split_horizon:<20} {passive:<10}")
|
||||
|
||||
|
||||
def show_rip_neighbors(json_data):
|
||||
"""Show RIP neighbor information"""
|
||||
routing = json_data.get('ietf-routing:routing', {})
|
||||
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
|
||||
|
||||
rip_instance = None
|
||||
for protocol in protocols:
|
||||
if 'ietf-rip:rip' in protocol:
|
||||
rip_instance = protocol
|
||||
break
|
||||
|
||||
if not rip_instance:
|
||||
print("RIP is not configured or running")
|
||||
return
|
||||
|
||||
rip = rip_instance.get('ietf-rip:rip', {})
|
||||
ipv4 = rip.get('ipv4', {})
|
||||
neighbors = ipv4.get('neighbors', {}).get('neighbor', [])
|
||||
|
||||
if not neighbors:
|
||||
print("No RIP neighbors")
|
||||
return
|
||||
|
||||
# Header
|
||||
hdr = f"{'ADDRESS':<16} {'BAD-PACKETS':<14} {'BAD-ROUTES':<12}"
|
||||
print(Decore.invert(hdr))
|
||||
|
||||
for neighbor in neighbors:
|
||||
address = neighbor.get('ipv4-address', 'unknown')
|
||||
bad_packets = neighbor.get('bad-packets-rcvd', 0)
|
||||
bad_routes = neighbor.get('bad-routes-rcvd', 0)
|
||||
|
||||
print(f"{address:<16} {bad_packets:<14} {bad_routes:<12}")
|
||||
|
||||
|
||||
def show_bfd_status(json_data):
|
||||
"""Show BFD status summary"""
|
||||
routing = json_data.get('ietf-routing:routing', {})
|
||||
@@ -3549,6 +3708,11 @@ def main():
|
||||
subparsers.add_parser('show-ospf-neighbor', help='Show OSPF neighbors')
|
||||
subparsers.add_parser('show-ospf-routes', help='Show OSPF routing table')
|
||||
|
||||
subparsers.add_parser('show-rip', help='Show RIP instance information')
|
||||
subparsers.add_parser('show-rip-routes', help='Show RIP routing table')
|
||||
subparsers.add_parser('show-rip-interfaces', help='Show RIP interfaces')
|
||||
subparsers.add_parser('show-rip-neighbors', help='Show RIP neighbors')
|
||||
|
||||
subparsers.add_parser('show-routing-table', help='Show the routing table') \
|
||||
.add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
|
||||
|
||||
@@ -3602,6 +3766,14 @@ def main():
|
||||
show_ospf_neighbor(json_data)
|
||||
elif args.command == "show-ospf-routes":
|
||||
show_ospf_routes(json_data)
|
||||
elif args.command == "show-rip":
|
||||
show_rip(json_data)
|
||||
elif args.command == "show-rip-routes":
|
||||
show_rip_routes(json_data)
|
||||
elif args.command == "show-rip-interfaces":
|
||||
show_rip_interfaces(json_data)
|
||||
elif args.command == "show-rip-neighbors":
|
||||
show_rip_neighbors(json_data)
|
||||
elif args.command == "show-routing-table":
|
||||
show_routing_table(json_data, args.ip)
|
||||
elif args.command == "show-software":
|
||||
|
||||
@@ -6,7 +6,8 @@ license = "MIT"
|
||||
packages = [
|
||||
{ include = "yanger" },
|
||||
{ include = "cli_pretty" },
|
||||
{ include = "ospf_status" }
|
||||
{ include = "ospf_status" },
|
||||
{ include = "rip_status" }
|
||||
]
|
||||
authors = [
|
||||
"KernelKit developers"
|
||||
@@ -21,3 +22,4 @@ build-backend = "poetry.core.masonry.api"
|
||||
yanger = "yanger.__main__:main"
|
||||
cli-pretty = "cli_pretty:main"
|
||||
ospf-status = "ospf_status:main"
|
||||
rip-status = "rip_status.rip_status:main"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/python3
|
||||
# This script is used to transform the output from the show ip rip commands
|
||||
# to match the ietf-rip YANG model structure.
|
||||
#
|
||||
# This makes the parsing for the operational parts of YANG model more easy
|
||||
#
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_json_cmd(cmd, default=None, check=True):
|
||||
"""Run a command (array of args) with JSON output and return the JSON"""
|
||||
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, json.JSONDecodeError):
|
||||
if default is not None:
|
||||
return default
|
||||
raise
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Collects RIP operational data from FRR vtysh and transforms it
|
||||
into a structure that matches the ietf-rip YANG model.
|
||||
"""
|
||||
rip_routes_cmd = ['sudo', 'vtysh', '-c', "show ip rip json"]
|
||||
rip_status_cmd = ['sudo', 'vtysh', '-c', "show ip rip status json"]
|
||||
|
||||
try:
|
||||
routes = run_json_cmd(rip_routes_cmd, default={})
|
||||
status = run_json_cmd(rip_status_cmd, default={})
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
# Build the structure matching ietf-rip YANG model
|
||||
result = {
|
||||
"routes": routes.get("routes", {}),
|
||||
"status": status
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
data = main()
|
||||
print(json.dumps(data, indent=2))
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -63,6 +63,9 @@ def main():
|
||||
elif args.model == 'ietf-ospf':
|
||||
from . import ietf_ospf
|
||||
yang_data = ietf_ospf.operational()
|
||||
elif args.model == 'ietf-rip':
|
||||
from . import ietf_rip
|
||||
yang_data = ietf_rip.operational()
|
||||
elif args.model == 'ietf-hardware':
|
||||
from . import ietf_hardware
|
||||
yang_data = ietf_hardware.operational()
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import re
|
||||
from .host import HOST
|
||||
|
||||
|
||||
def parse_rip_status():
|
||||
"""Parse 'show ip rip status' text output to extract operational state
|
||||
|
||||
Returns dict with keys: update-interval, invalid-interval, flush-interval,
|
||||
default-metric, distance, interfaces (list), neighbors (list)
|
||||
"""
|
||||
try:
|
||||
# HOST.run expects tuple, returns text string directly
|
||||
text = HOST.run(tuple(['vtysh', '-c', 'show ip rip status']), default="")
|
||||
if not text:
|
||||
return {}
|
||||
except Exception as e:
|
||||
return {}
|
||||
|
||||
status = {}
|
||||
|
||||
# Parse: "Sending updates every 30 seconds"
|
||||
match = re.search(r'Sending updates every (\d+) seconds', text)
|
||||
if match:
|
||||
status['update-interval'] = int(match.group(1))
|
||||
|
||||
# Parse: "Timeout after 180 seconds"
|
||||
match = re.search(r'Timeout after (\d+) seconds', text)
|
||||
if match:
|
||||
status['invalid-interval'] = int(match.group(1))
|
||||
|
||||
# Parse: "garbage collect after 240 seconds"
|
||||
match = re.search(r'garbage collect after (\d+) seconds', text)
|
||||
if match:
|
||||
status['flush-interval'] = int(match.group(1))
|
||||
|
||||
# Parse: "Default redistribution metric is 1"
|
||||
match = re.search(r'Default redistribution metric is (\d+)', text)
|
||||
if match:
|
||||
status['default-metric'] = int(match.group(1))
|
||||
|
||||
# Parse: "Distance: (default is 120)"
|
||||
match = re.search(r'Distance: \(default is (\d+)\)', text)
|
||||
if match:
|
||||
status['distance'] = int(match.group(1))
|
||||
|
||||
# Parse interface table:
|
||||
# Interface Send Recv Key-chain
|
||||
# e5 2 2
|
||||
interfaces = []
|
||||
in_interface_section = False
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
# Detect start of interface section
|
||||
if 'Interface' in line and 'Send' in line and 'Recv' in line:
|
||||
in_interface_section = True
|
||||
continue
|
||||
|
||||
# Stop at next section
|
||||
if in_interface_section and (line.startswith('Routing for Networks:') or
|
||||
line.startswith('Routing Information Sources:')):
|
||||
break
|
||||
|
||||
# Parse interface lines
|
||||
if in_interface_section and line:
|
||||
# Format: "e5 2 2 "
|
||||
parts = line.split()
|
||||
if len(parts) >= 3 and not line.startswith('Interface'):
|
||||
iface_name = parts[0]
|
||||
send_version = parts[1]
|
||||
recv_version = parts[2]
|
||||
|
||||
interfaces.append({
|
||||
'name': iface_name,
|
||||
'send-version': int(send_version),
|
||||
'recv-version': int(recv_version)
|
||||
})
|
||||
|
||||
if interfaces:
|
||||
status['interfaces'] = interfaces
|
||||
|
||||
# Parse Routing Information Sources table:
|
||||
# Gateway BadPackets BadRoutes Distance Last Update
|
||||
# 192.168.50.2 0 0 120 00:00:09
|
||||
neighbors = []
|
||||
in_neighbor_section = False
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
# Detect start of Routing Information Sources section
|
||||
if line.startswith('Routing Information Sources:'):
|
||||
in_neighbor_section = True
|
||||
continue
|
||||
|
||||
# Skip the header line
|
||||
if in_neighbor_section and 'Gateway' in line and 'BadPackets' in line:
|
||||
continue
|
||||
|
||||
# Stop at next section (Distance line or empty lines after table)
|
||||
if in_neighbor_section and (line.startswith('Distance:') or
|
||||
(not line and neighbors)):
|
||||
break
|
||||
|
||||
# Parse neighbor lines
|
||||
if in_neighbor_section and line:
|
||||
# Format: "192.168.50.2 0 0 120 00:00:09"
|
||||
parts = line.split()
|
||||
if len(parts) >= 5:
|
||||
try:
|
||||
gateway = parts[0]
|
||||
bad_packets = int(parts[1])
|
||||
bad_routes = int(parts[2])
|
||||
distance = int(parts[3])
|
||||
last_update = parts[4] # Format: HH:MM:SS
|
||||
|
||||
neighbors.append({
|
||||
'address': gateway,
|
||||
'bad-packets': bad_packets,
|
||||
'bad-routes': bad_routes,
|
||||
'distance': distance,
|
||||
'last-update': last_update
|
||||
})
|
||||
except (ValueError, IndexError):
|
||||
# Skip lines that don't parse correctly
|
||||
continue
|
||||
|
||||
if neighbors:
|
||||
status['neighbors'] = neighbors
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def add_rip(control_protocols):
|
||||
"""Populate RIP operational data
|
||||
|
||||
Note: FRR's RIP implementation provides JSON for routing table but not for
|
||||
status commands, so we combine JSON route data with text parsing for status.
|
||||
"""
|
||||
# Get operational status from text parsing
|
||||
status = parse_rip_status()
|
||||
|
||||
# Check if RIP is running - if we can't get status, it's probably not running
|
||||
if not status:
|
||||
return
|
||||
|
||||
control_protocol = {}
|
||||
control_protocol["type"] = "infix-routing:ripv2"
|
||||
control_protocol["name"] = "default"
|
||||
control_protocol["ietf-rip:rip"] = {}
|
||||
|
||||
rip = control_protocol["ietf-rip:rip"]
|
||||
|
||||
# Add global operational state
|
||||
if status.get('distance'):
|
||||
rip['distance'] = status['distance']
|
||||
if status.get('default-metric'):
|
||||
rip['default-metric'] = status['default-metric']
|
||||
|
||||
# Add timers if available
|
||||
if any(k in status for k in ['update-interval', 'invalid-interval', 'flush-interval']):
|
||||
rip['timers'] = {}
|
||||
if status.get('update-interval'):
|
||||
rip['timers']['update-interval'] = status['update-interval']
|
||||
if status.get('invalid-interval'):
|
||||
rip['timers']['invalid-interval'] = status['invalid-interval']
|
||||
if status.get('flush-interval'):
|
||||
rip['timers']['flush-interval'] = status['flush-interval']
|
||||
|
||||
# Add interfaces if available
|
||||
if status.get('interfaces'):
|
||||
rip['interfaces'] = {'interface': []}
|
||||
for iface in status['interfaces']:
|
||||
iface_data = {
|
||||
'interface': iface['name'],
|
||||
'oper-status': 'up' # If it's in the list, it's operational
|
||||
}
|
||||
|
||||
# Map FRR version numbers to YANG enum values
|
||||
# FRR shows: 1=v1, 2=v2, and we assume if both are enabled it would show differently
|
||||
send_ver = iface.get('send-version')
|
||||
if send_ver == 1:
|
||||
iface_data['send-version'] = '1'
|
||||
elif send_ver == 2:
|
||||
iface_data['send-version'] = '2'
|
||||
# Note: FRR might show this differently for mixed mode
|
||||
|
||||
recv_ver = iface.get('recv-version')
|
||||
if recv_ver == 1:
|
||||
iface_data['receive-version'] = '1'
|
||||
elif recv_ver == 2:
|
||||
iface_data['receive-version'] = '2'
|
||||
|
||||
rip['interfaces']['interface'].append(iface_data)
|
||||
|
||||
# Get RIP-learned routes from routing table (JSON)
|
||||
# This shows routes learned via RIP (R(n) entries), not redistributed routes
|
||||
route_data = HOST.run_json(['vtysh', '-c', 'show ip route rip json'], default={})
|
||||
|
||||
routes = []
|
||||
for prefix, entries in route_data.items():
|
||||
if not entries or '/' not in prefix:
|
||||
continue
|
||||
|
||||
# Use first entry (RIP doesn't typically have ECMP)
|
||||
entry = entries[0] if isinstance(entries, list) else entries
|
||||
|
||||
route = {
|
||||
"ipv4-prefix": prefix,
|
||||
"metric": entry.get("metric", 0),
|
||||
"route-type": "rip"
|
||||
}
|
||||
|
||||
# Get next hop information
|
||||
nexthops = entry.get("nexthops", [])
|
||||
if nexthops:
|
||||
first_hop = nexthops[0]
|
||||
if first_hop.get("ip"):
|
||||
route["next-hop"] = first_hop["ip"]
|
||||
if first_hop.get("interfaceName"):
|
||||
route["interface"] = first_hop["interfaceName"]
|
||||
|
||||
routes.append(route)
|
||||
|
||||
# Add neighbors to operational data
|
||||
neighbors_list = []
|
||||
if status.get('neighbors'):
|
||||
for neighbor in status['neighbors']:
|
||||
neighbor_data = {
|
||||
'ipv4-address': neighbor['address'],
|
||||
'bad-packets-rcvd': neighbor['bad-packets'],
|
||||
'bad-routes-rcvd': neighbor['bad-routes']
|
||||
}
|
||||
# Note: IETF YANG expects last-update as yang:date-and-time
|
||||
# but FRR gives us a relative time like "00:00:09"
|
||||
# We'll skip last-update for now or convert it if needed
|
||||
neighbors_list.append(neighbor_data)
|
||||
|
||||
# Add routes and neighbors to operational data
|
||||
if routes or neighbors_list:
|
||||
if "ipv4" not in rip:
|
||||
rip["ipv4"] = {}
|
||||
|
||||
if routes:
|
||||
rip["ipv4"]["routes"] = {
|
||||
"route": routes
|
||||
}
|
||||
# Add route count
|
||||
rip["num-of-routes"] = len(routes)
|
||||
|
||||
if neighbors_list:
|
||||
rip["ipv4"]["neighbors"] = {
|
||||
"neighbor": neighbors_list
|
||||
}
|
||||
|
||||
# Add the control-protocol
|
||||
if "ietf-routing:control-plane-protocol" not in control_protocols:
|
||||
control_protocols["ietf-routing:control-plane-protocol"] = []
|
||||
control_protocols["ietf-routing:control-plane-protocol"].append(control_protocol)
|
||||
|
||||
|
||||
def operational():
|
||||
"""Return RIP operational data in YANG format"""
|
||||
out = {
|
||||
"ietf-routing:routing": {
|
||||
"control-plane-protocols": {}
|
||||
}
|
||||
}
|
||||
|
||||
add_rip(out['ietf-routing:routing']['control-plane-protocols'])
|
||||
return out
|
||||
@@ -51,6 +51,7 @@ def add_protocol(routes, proto):
|
||||
'static': 'static',
|
||||
'ospf': 'ietf-ospf:ospfv2',
|
||||
'ospf6': 'ietf-ospf:ospfv3',
|
||||
'rip': 'ietf-rip:rip',
|
||||
}
|
||||
|
||||
out = {}
|
||||
@@ -75,9 +76,11 @@ def add_protocol(routes, proto):
|
||||
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
|
||||
# Metric only available in the model for OSPF and RIP routes
|
||||
if 'ospf' in frr:
|
||||
new['ietf-ospf:metric'] = route.get('metric', 0)
|
||||
elif 'rip' in frr:
|
||||
new['ietf-rip: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]
|
||||
|
||||
Reference in New Issue
Block a user