From cb2947401f1851815d3edfebd2edbd61ccc01c48 Mon Sep 17 00:00:00 2001 From: Roger Luethi Date: Tue, 25 Aug 2026 15:28:16 +0200 Subject: [PATCH] fix(sonic): emit up/down for BGP_NEIGHBOR_AF The generator wrote {"admin_status": "true"} for every BGP_NEIGHBOR_AF row. The ConfigDB YANG typedef admin_status is a strict enumeration of up and down (sonic-types.yang.j2), so "true" is outside the type, and upstream's own YANG test config for this table uses "up". Every other table the generator writes -- PORT, PORTCHANNEL, LOOPBACK, VLAN, VLAN_INTERFACE and the MGMT_ tables -- already emits up/down, which left BGP_NEIGHBOR_AF as the lone outlier. This is not cosmetic. config reload runs YANG validation and aborts on failure. On sonic-utilities master the no-filename form validates /etc/sonic/config_db.json, which is exactly how this project applies a generated config, so "true" becomes a hard failure in a future release; on 202405 and later an explicit-filename reload already aborts. Behaviour on the switch is unchanged, because frrcfgd converts up and down to true and false internally. Switches already running a generated config hold "true" and will show the new value as a diff the next time their config is regenerated. The bundled validator rejected every generated AF row before this change and accepts them now, which is what allows osism sonic validate to be used as a gate. Twelve assertions in the BGP test module are updated to match. Assisted-by: Claude:claude-opus-5 Signed-off-by: Roger Luethi --- .../tasks/conductor/sonic/config_generator.py | 14 +++++------ .../test_config_generator_bgp_vlan_vrf.py | 24 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/osism/tasks/conductor/sonic/config_generator.py b/osism/tasks/conductor/sonic/config_generator.py index a48d2225d..2ebef2850 100644 --- a/osism/tasks/conductor/sonic/config_generator.py +++ b/osism/tasks/conductor/sonic/config_generator.py @@ -1258,12 +1258,12 @@ def get_vrf_for_interface(interface_name): vrf_name = get_vrf_for_interface(port_name) ipv4_key = f"{vrf_name}|{neighbor_id}|{BGP_AF_IPV4_UNICAST}" - config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"} + config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "up"} # Only add ipv6_unicast if v6only would be true (no transfer role IPv4) if not has_transfer_ipv4: ipv6_key = f"{vrf_name}|{neighbor_id}|{BGP_AF_IPV6_UNICAST}" - config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"} + config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "up"} logger.debug( f"Added BGP_NEIGHBOR_AF with ipv4_unicast and ipv6_unicast for interface {port_name} (no direct IPv4)" ) @@ -1292,7 +1292,7 @@ def get_vrf_for_interface(interface_name): if is_switch_connection or has_l2vpn_tag: l2vpn_key = f"{vrf_name}|{neighbor_id}|l2vpn_evpn" - config["BGP_NEIGHBOR_AF"][l2vpn_key] = {"admin_status": "true"} + config["BGP_NEIGHBOR_AF"][l2vpn_key] = {"admin_status": "up"} logger.debug( f"Added BGP_NEIGHBOR_AF l2vpn_evpn for interface {port_name} " f"(connected to {connected_device.name})" @@ -1334,8 +1334,8 @@ def get_vrf_for_interface(interface_name): ipv4_key = f"{vrf_name}|{neighbor_id}|{BGP_AF_IPV4_UNICAST}" ipv6_key = f"{vrf_name}|{neighbor_id}|{BGP_AF_IPV6_UNICAST}" - config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"} - config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"} + config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "up"} + config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "up"} # Add l2vpn_evpn only for switch-to-switch connections (default VRF) if vrf_name == "default": @@ -1348,7 +1348,7 @@ def get_vrf_for_interface(interface_name): ) if is_switch_connection: l2vpn_key = f"{vrf_name}|{neighbor_id}|l2vpn_evpn" - config["BGP_NEIGHBOR_AF"][l2vpn_key] = {"admin_status": "true"} + config["BGP_NEIGHBOR_AF"][l2vpn_key] = {"admin_status": "up"} logger.debug( f"Added BGP_NEIGHBOR_AF l2vpn_evpn for port channel {pc_name} " f"(connected to switch {connected_device.name})" @@ -1645,7 +1645,7 @@ def get_vrf_for_interface(interface_name): # Add BGP_NEIGHBOR_AF for the matching address family af_key = f"{vrf_name}|{peer_ip}|{address_family}" - config["BGP_NEIGHBOR_AF"][af_key] = {"admin_status": "true"} + config["BGP_NEIGHBOR_AF"][af_key] = {"admin_status": "up"} logger.info( f"Added BGP neighbor configuration for VLAN {vid} using peer IP {peer_ip} " diff --git a/tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.py b/tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.py index 3cf85a92a..6e787bc98 100644 --- a/tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.py +++ b/tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.py @@ -132,21 +132,21 @@ def test_direct_ipv4_excluded_and_logged(self, bgp_config, patch_bgp, loguru_log def test_no_direct_ipv4_adds_ipv4_and_ipv6(self, bgp_config, patch_bgp): self._base(bgp_config, interface_ips={}, transfer_ips={}) af = bgp_config["BGP_NEIGHBOR_AF"] - assert af["default|Ethernet0|ipv4_unicast"] == {"admin_status": "true"} - assert af["default|Ethernet0|ipv6_unicast"] == {"admin_status": "true"} + assert af["default|Ethernet0|ipv4_unicast"] == {"admin_status": "up"} + assert af["default|Ethernet0|ipv6_unicast"] == {"admin_status": "up"} assert "default|Ethernet0|l2vpn_evpn" not in af def test_transfer_role_ipv4_adds_ipv4_only(self, bgp_config, patch_bgp): self._base(bgp_config, transfer_ips={"eth0": "10.0.0.1/31"}) af = bgp_config["BGP_NEIGHBOR_AF"] - assert af["default|Ethernet0|ipv4_unicast"] == {"admin_status": "true"} + assert af["default|Ethernet0|ipv4_unicast"] == {"admin_status": "up"} assert "default|Ethernet0|ipv6_unicast" not in af def test_switch_to_switch_adds_l2vpn(self, bgp_config, patch_bgp): patch_bgp.connected_device.return_value = _switch_device() self._base(bgp_config) assert bgp_config["BGP_NEIGHBOR_AF"]["default|Ethernet0|l2vpn_evpn"] == { - "admin_status": "true" + "admin_status": "up" } def test_non_switch_with_l2vpn_tag_adds_l2vpn(self, bgp_config, patch_bgp): @@ -198,8 +198,8 @@ class TestBgpNeighborAfPortChannels: def test_adds_ipv4_and_ipv6(self, bgp_config, patch_bgp): _call_bgp(bgp_config, connected_portchannels={"PortChannel1"}) af = bgp_config["BGP_NEIGHBOR_AF"] - assert af["default|PortChannel1|ipv4_unicast"] == {"admin_status": "true"} - assert af["default|PortChannel1|ipv6_unicast"] == {"admin_status": "true"} + assert af["default|PortChannel1|ipv4_unicast"] == {"admin_status": "up"} + assert af["default|PortChannel1|ipv6_unicast"] == {"admin_status": "up"} def test_switch_connection_adds_l2vpn(self, bgp_config, patch_bgp): patch_bgp.connected_device.return_value = _switch_device() @@ -365,7 +365,7 @@ def test_untagged_member_with_peer_ip(self, bgp_config, patch_bgp): "v6only": "false", } assert bgp_config["BGP_NEIGHBOR_AF"]["default|192.0.2.20|ipv4_unicast"] == { - "admin_status": "true" + "admin_status": "up" } def test_untagged_member_with_peer_ipv6(self, bgp_config, patch_bgp): @@ -384,7 +384,7 @@ def test_untagged_member_with_peer_ipv6(self, bgp_config, patch_bgp): "v6only": "false", } assert bgp_config["BGP_NEIGHBOR_AF"]["default|2001:db8::20|ipv6_unicast"] == { - "admin_status": "true" + "admin_status": "up" } assert "default|2001:db8::20|ipv4_unicast" not in bgp_config["BGP_NEIGHBOR_AF"] @@ -404,10 +404,10 @@ def test_dual_stack_peer_adds_both_neighbors(self, bgp_config, patch_bgp): "default|2001:db8::20", } assert bgp_config["BGP_NEIGHBOR_AF"]["default|192.0.2.20|ipv4_unicast"] == { - "admin_status": "true" + "admin_status": "up" } assert bgp_config["BGP_NEIGHBOR_AF"]["default|2001:db8::20|ipv6_unicast"] == { - "admin_status": "true" + "admin_status": "up" } def test_ipv4_only_svi_skips_ipv6_peer(self, bgp_config, patch_bgp): @@ -1072,8 +1072,8 @@ def test_svi_peer_resolved_for_untagged_port_channel(self, bgp_config, patch_bgp "v6only": "false", } assert bgp_config["BGP_NEIGHBOR_AF"] == { - "default|192.0.2.1|ipv4_unicast": {"admin_status": "true"}, - "default|2001:db8::1|ipv6_unicast": {"admin_status": "true"}, + "default|192.0.2.1|ipv4_unicast": {"admin_status": "up"}, + "default|2001:db8::1|ipv6_unicast": {"admin_status": "up"}, } # The SVI lookup is the one caller that opts into the LAG member walk; # without the flag the resolver stops at the uncabled bundle and this