From dd3f1c39d14e5f6e872770f0408dfcb4be37ab0f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 23 Sep 2024 10:23:42 +0200 Subject: [PATCH] confd: expand route info in operational data - Enable OSPF route metrics in RIB, only defined for OSPF - Enable 'active' route flag for RIB, same as 'selected >' in FRR - Enable 'last-updated' route field for RIB, used for uptime - Augment route in RIB with 'installed' flag, same as 'FIB *' in FRR The CLI "show ip route" command has been updated accordingly. The classic Cisco style output has been kept instead of separating the routes into Active and Backup, as discussed previously. Because when inspecting large route sets you want to compare same prefix vs distance/metric to understand why a certain route is selected. admin@example:/> show ip route PREFIX PREF NEXT-HOP PROTOCOL UPTIME > 0.0.0.0/0 1/0 192.168.0.42 static 0h2m6s * 192.168.122.1 0.0.0.0/0 5/0 192.168.122.1 static 0h2m6s 0.0.0.0/0 254/0 e1 static 0h2m6s >* 169.254.0.0/16 0/0 e1 direct 0h2m6s >* 192.168.0.0/24 1/0 192.168.122.1 static 0h2m6s >* 192.168.122.0/24 0/0 e1 direct 0h2m12s Signed-off-by: Joachim Wiberg --- src/confd/yang/infix-routing@2024-09-22.yang | 35 +++++----- src/statd/python/cli_pretty/cli_pretty.py | 68 ++++++++++++++++---- src/statd/python/yanger/yanger.py | 28 +++++++- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/src/confd/yang/infix-routing@2024-09-22.yang b/src/confd/yang/infix-routing@2024-09-22.yang index 8b8c196e..b8b9f3b7 100644 --- a/src/confd/yang/infix-routing@2024-09-22.yang +++ b/src/confd/yang/infix-routing@2024-09-22.yang @@ -2,30 +2,32 @@ module infix-routing { yang-version 1.1; namespace "urn:infix:routing:ns:yang:1.0"; prefix infix-rt; + import ietf-routing { prefix rt; } - import ietf-ipv4-unicast-routing { prefix v4ur; } - import ietf-ipv6-unicast-routing { prefix v6ur; } import ietf-ospf { prefix ospf; } - import ietf-interfaces { prefix if; } - import ietf-routing-types { prefix rt-types; } + revision 2024-09-22 { - description "Adjust YANG prefix for consistency: ietf-r: -> rt:"; + description "- Enable OSPF route metrics in RIB + - Enable 'active' route flag for RIB + - Enable 'last-updated' route field for RIB + - Augment route in RIB with 'installed' flag + - Aadjust YANG prefix for consistency: ietf-r: -> rt:"; reference "internal"; } revision 2024-09-21 { @@ -130,26 +132,27 @@ module infix-routing { deviation "/rt:routing/rt:ribs/rt:rib/rt:active-route" { deviate not-supported; } - - deviation "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/ospf:metric" { - deviate not-supported; - } - deviation "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/ospf:tag" { deviate not-supported; } deviation "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/ospf:route-type" { deviate not-supported; } - deviation "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/rt:active" { - deviate not-supported; - } - deviation "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/rt:last-updated" { - deviate not-supported; - } deviation "/rt:routing/rt:ribs/rt:rib/rt:description" { deviate not-supported; } + augment "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/" + + "rt:next-hop/rt:next-hop-options/rt:next-hop-list/" + + "rt:next-hop-list/rt:next-hop" { + description "IETF use 'active' for selected routes within a protocol (RIB). + This augment adds an 'installed' leaf for marking routes that + are installed in the kernel FIB with the given next-hop."; + leaf installed { + description "The presence of this leaf indicates that the route is + installed in the kernel FIB with the given next-hop."; + type empty; + } + } /* OSPF */ typedef infix-ospf-interface-type { diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 01495a3b..94f93703 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -3,6 +3,7 @@ import json import argparse import sys import re +from datetime import datetime, timezone class Pad: @@ -21,9 +22,10 @@ class PadMdb: class PadRoute: prefix = 30 - protocol = 10 - next_hop = 30 pref = 8 + next_hop = 30 + protocol = 10 + uptime = 10 class PadSoftware: @@ -88,17 +90,23 @@ class Route: self.data = data self.prefix = data.get(f'ietf-{ip}-unicast-routing:destination-prefix', '') self.protocol = data.get('source-protocol', '').split(':')[-1] + self.last_updated = data.get('last-updated', '') + self.active = data.get('active', False) self.pref = data.get('route-preference', '') + self.metric = data.get('ietf-ospf:metric', 0) self.next_hop = [] next_hop_list = get_json_data(None, self.data, 'next-hop', 'next-hop-list') if next_hop_list: for nh in next_hop_list["next-hop"]: if nh.get(f"ietf-{ip}-unicast-routing:address"): - self.next_hop.append(nh[f"ietf-{ip}-unicast-routing:address"]) + hop = nh[f"ietf-{ip}-unicast-routing:address"] elif nh.get("outgoing-interface"): - self.next_hop.append(nh["outgoing-interface"]) + hop = nh["outgoing-interface"] else: - self.next_hop.append("unspecified") + hop = "unspecified" + + fib = nh.get('infix-routing:installed', False) + self.next_hop.append((hop, fib)) else: interface = get_json_data(None, self.data, 'next-hop', 'outgoing-interface') address = get_json_data(None, self.data, 'next-hop', f'ietf-{ip}-unicast-routing:next-hop-address') @@ -116,23 +124,56 @@ class Route: def get_distance_and_metric(self): if isinstance(self.pref, int): distance = self.pref - metric = 0 + metric = self.metric else: distance, metric = 0, 0 return distance, metric + def datetime2uptime(self): + """Convert 'last-updated' string to uptime in AAhBBmCCs format.""" + if not self.last_updated: + return "0h0m0s" + + # Replace the colon in the timezone offset (e.g., +00:00 -> +0000) + pos = self.last_updated.rfind('+') + if pos != -1: + adjusted = self.last_updated[:pos] + self.last_updated[pos:].replace(':', '') + else: + adjusted = self.last_updated + + last_updated = datetime.strptime(adjusted, '%Y-%m-%dT%H:%M:%S%z') + current_time = datetime.now(timezone.utc) + uptime_delta = current_time - last_updated + + hours, remainder = divmod(int(uptime_delta.total_seconds()), 3600) + minutes, seconds = divmod(remainder, 60) + + return f"{hours}h{minutes}m{seconds}s" + def print(self): distance, metric = self.get_distance_and_metric() + uptime = self.datetime2uptime() pref = f"{distance}/{metric}" - row = f"{self.prefix:<{PadRoute.prefix}}" - row += f"{self.next_hop[0]:<{PadRoute.next_hop}}" + hop, fib = self.next_hop[0] + + row = ">" if self.active else " " + row += "*" if fib else " " + row += " " + row += f"{self.prefix:<{PadRoute.prefix}}" row += f"{pref:>{PadRoute.pref}} " + row += f"{hop:<{PadRoute.next_hop}}" row += f"{self.protocol:<{PadRoute.protocol}}" + row += f"{uptime:>{PadRoute.uptime}}" print(row) for nh in self.next_hop[1:]: - row = f"{'':<{PadRoute.prefix}}" - row += f"{nh:<{PadRoute.next_hop}}" + hop, fib = nh + row = " " + row += "*" if fib else " " + row += " " + row += f"{'':<{PadRoute.prefix}}" + row += f"{'':>{PadRoute.pref}} " + row += f"{hop:<{PadRoute.next_hop}}" print(row) @@ -555,10 +596,11 @@ def show_routing_table(json, ip): print("Error, top level \"ietf-routing:routing\" missing") sys.exit(1) - hdr = (f"{'PREFIX':<{PadRoute.prefix}}" - f"{'NEXT-HOP':<{PadRoute.next_hop}}" + hdr = (f" {'PREFIX':<{PadRoute.prefix}}" f"{'PREF':>{PadRoute.pref}} " - f"{'PROTOCOL':<{PadRoute.protocol}}") + f"{'NEXT-HOP':<{PadRoute.next_hop}}" + f"{'PROTOCOL':<{PadRoute.protocol}}" + f"{'UPTIME':>{PadRoute.uptime}}") print(Decore.invert(hdr)) for rib in get_json_data({}, json, 'ietf-routing:routing', 'ribs', 'rib'): diff --git a/src/statd/python/yanger/yanger.py b/src/statd/python/yanger/yanger.py index fc2e55fa..b2a3c332 100755 --- a/src/statd/python/yanger/yanger.py +++ b/src/statd/python/yanger/yanger.py @@ -6,7 +6,7 @@ import json import sys # (built-in module) import os import argparse -from datetime import datetime +from datetime import datetime, timedelta, timezone TESTPATH = "" logger = None @@ -230,6 +230,17 @@ def add_hardware(hw_out): insert(hw_out, "component", components) +def uptime2datetime(uptime): + """Convert uptime (HH:MM:SS) to YANG format (YYYY-MM-DDTHH:MM:SS+00:00)""" + h, m, s = map(int, uptime.split(':')) + uptime_delta = timedelta(hours=h, minutes=m, seconds=s) + current_time = datetime.now(timezone.utc) + 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""" @@ -264,6 +275,18 @@ def get_routes(routes, proto, data): 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 = {} @@ -271,6 +294,9 @@ def get_routes(routes, proto, data): 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: