From 024b51cc6a22b5a674d2421382c6ccc5f6f8385b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 5 Dec 2025 10:54:08 +0100 Subject: [PATCH] statd: add support for ip forwarding operational state This patch unlocks "routing interfaces" support in the ietf-routing yang model. An array of interface with IP forwarding enabled. Note, because of #515 we skip IPv6 forwarding for now. This will in the near future be handled by a per-interface force_forwarding sysctl flag. The 'show interface [ifname]' admin-exec command has been extended with a Flags field for a quick overview of which interfaces have forwarding currently enabled. Fixes #647 Signed-off-by: Joachim Wiberg --- src/confd/yang/confd/infix-routing.yang | 4 --- src/show/show.py | 6 ++++ src/statd/python/cli_pretty/cli_pretty.py | 40 ++++++++++++++++++++--- src/statd/python/yanger/ietf_routing.py | 28 ++++++++++++++++ 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/confd/yang/confd/infix-routing.yang b/src/confd/yang/confd/infix-routing.yang index f0f23464..c17f7d56 100644 --- a/src/confd/yang/confd/infix-routing.yang +++ b/src/confd/yang/confd/infix-routing.yang @@ -111,10 +111,6 @@ module infix-routing { } /* General routing */ - deviation "/rt:routing/rt:interfaces" { - description "Initial limitation"; - deviate not-supported; - } deviation "/rt:routing/rt:router-id" { description "Set in OSPF"; diff --git a/src/show/show.py b/src/show/show.py index c2eb6e63..84990168 100755 --- a/src/show/show.py +++ b/src/show/show.py @@ -95,6 +95,12 @@ def interface(args: List[str]) -> None: print("No interface data retrieved.") return + # Also fetch routing interface list for forwarding indication + routing_data = run_sysrepocfg("/ietf-routing:routing") + if routing_data: + # Merge routing data into the main data structure + data.update(routing_data) + if RAW_OUTPUT: print(json.dumps(data, indent=2)) return diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 55337ec5..c85a1db8 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -98,6 +98,7 @@ def compress_interface_list(interfaces): class Pad: + flags = 2 iface = 16 proto = 11 state = 12 @@ -803,6 +804,9 @@ class DhcpServer: class Iface: + # Class variable to hold routing-enabled interfaces for the current display session + _routing_ifaces = set() + def __init__(self, data): self.data = data self.name = data.get('name', '') @@ -915,13 +919,16 @@ class Iface: return self.oper_status def pr_name(self, pipe=""): + flag = "⇅" if self.name in Iface._routing_ifaces else " " + print(f"{flag:<{Pad.flags}}", end="") print(f"{pipe}{self.name:<{Pad.iface - len(pipe)}}", end="") def pr_proto_ipv4(self, pipe=''): for addr in self.ipv4_addr: origin = f"({addr['origin']})" if addr.get('origin') else "" - row = f"{pipe:<{Pad.iface}}" + row = f"{'':<{Pad.flags}}" + row += f"{pipe:<{Pad.iface}}" row += f"{'ipv4':<{Pad.proto}}" row += f"{'':<{Pad.state}}{addr['ip']}/{addr['prefix-length']} {origin}" print(row) @@ -930,7 +937,8 @@ class Iface: for addr in self.ipv6_addr: origin = f"({addr['origin']})" if addr.get('origin') else "" - row = f"{pipe:<{Pad.iface}}" + row = f"{'':<{Pad.flags}}" + row += f"{pipe:<{Pad.iface}}" row += f"{'ipv6':<{Pad.proto}}" row += f"{'':<{Pad.state}}{addr['ip']}/{addr['prefix-length']} {origin}" print(row) @@ -938,7 +946,8 @@ class Iface: def _pr_proto_common(self, name, phys_address, pipe=''): row = "" if len(pipe) > 0: - row = f"{pipe:<{Pad.iface}}" + row = f"{'':<{Pad.flags}}" + row += f"{pipe:<{Pad.iface}}" row += f"{name:<{Pad.proto}}" dec = Decore.green if self.oper() == "up" else Decore.red @@ -1184,6 +1193,11 @@ class Iface: parent.pr_proto_eth() def pr_container(self): + # Add ⇅ flag for interfaces with IP forwarding enabled + flag = "⇅" if self.name in Iface._routing_ifaces else " " + # Flag column is outside the gray background + print(f"{flag:<{Pad.flags}}", end="") + row = f"{self.name:<{Pad.iface}}" row += f"{'container':<{Pad.proto}}" row += f"{'':<{Pad.state}}" @@ -1204,6 +1218,9 @@ class Iface: if self.oper(): print(f"{'operational status':<{20}}: {self.oper(detail=True)}") + forwarding = "enabled" if self.name in Iface._routing_ifaces else "disabled" + print(f"{'ip forwarding':<{20}}: {forwarding}") + if self.lower_if: print(f"{'lower-layer-if':<{20}}: {self.lower_if}") @@ -1407,13 +1424,21 @@ def print_interface(iface): def pr_interface_list(json): - hdr = (f"{'INTERFACE':<{Pad.iface}}" + hdr = (f"{'⚑':<{Pad.flags}}" + f"{'INTERFACE':<{Pad.iface}}" f"{'PROTOCOL':<{Pad.proto}}" f"{'STATE':<{Pad.state}}" f"{'DATA':<{Pad.data}}") print(Decore.invert(hdr)) + # Set class variable with routing-enabled interfaces (with IP forwarding) + if "ietf-routing:routing" in json: + routing_data = json["ietf-routing:routing"].get("interfaces", {}) + Iface._routing_ifaces = set(routing_data.get("interface", [])) + else: + Iface._routing_ifaces = set() + ifaces = sorted(json["ietf-interfaces:interfaces"]["interface"], key=ifname_sort) iface = find_iface(ifaces, "lo") @@ -1472,6 +1497,13 @@ def pr_interface_list(json): def show_interfaces(json, name): + # Set class variable with routing-enabled interfaces (with IP forwarding) + if "ietf-routing:routing" in json: + routing_data = json["ietf-routing:routing"].get("interfaces", {}) + Iface._routing_ifaces = set(routing_data.get("interface", [])) + else: + Iface._routing_ifaces = set() + if name: if not json.get("ietf-interfaces:interfaces"): print(f"No interface data found for \"{name}\"") diff --git a/src/statd/python/yanger/ietf_routing.py b/src/statd/python/yanger/ietf_routing.py index bab3741f..02444243 100644 --- a/src/statd/python/yanger/ietf_routing.py +++ b/src/statd/python/yanger/ietf_routing.py @@ -124,9 +124,37 @@ def add_protocol(routes, proto): insert(routes, 'routes', out) +def get_routing_interfaces(): + """Get list of interfaces with IPv4 forwarding enabled""" + import json + + # Get all interfaces + links_json = HOST.run(tuple(['ip', '-j', 'link', 'show']), default="[]") + links = json.loads(links_json) + + routing_ifaces = [] + for link in links: + ifname = link.get('ifname') + if not ifname: + continue + + # Check if IPv4 forwarding is enabled + # Note: We only check IPv4 forwarding. IPv6 forwarding behaves differently + # and will be handled separately when Linux 6.17+ force_forwarding is available. + ipv4_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv4.conf.{ifname}.forwarding']), default="0").strip() + + if ipv4_fwd == "1": + routing_ifaces.append(ifname) + + return routing_ifaces + + def operational(): out = { "ietf-routing:routing": { + "interfaces": { + "interface": get_routing_interfaces() + }, "ribs": { "rib": [{ "name": "ipv4",