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: