Background
Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 3 (#2199). Companion issue to the config_generator.py orchestrator and ports issues. Covers BGP, VLAN, Loopback, and VRF helpers in osism/tasks/conductor/sonic/config_generator.py.
Scope
Add tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.py covering the helpers below in osism/tasks/conductor/sonic/config_generator.py.
Test targets
_add_bgp_configurations(config, connected_interfaces, connected_portchannels, portchannel_info, device, device_as_mapping=None, interface_ips=None, netbox_interfaces=None, transfer_ips=None, netbox=None, vlan_info=None, vrf_info=None) — config_generator.py:914
Patch osism.tasks.conductor.sonic.config_generator.get_connected_device_for_sonic_interface and ...get_connected_interface_ipv4_address.
BGP_NEIGHBOR_AF for connected interfaces (lines 951–1039)
- Untagged VLAN member → excluded (the VLAN-SVI branch handles it instead)
- Direct IPv4 (no transfer role) → excluded from BGP, log message recorded
- No direct IPv4 → ipv4_unicast + ipv6_unicast entries under
default|<port>|...
- Transfer-role IPv4 → ipv4_unicast only (
v6only=false semantics deferred to BGP_NEIGHBOR test below)
- Switch-to-switch (
connected_device.role.slug ∈ DEFAULT_SONIC_ROLES) → l2vpn_evpn entry added
- Non-switch connected device but interface has the
BGP_AF_L2VPN_EVPN_TAG tag → l2vpn_evpn entry added
- Non-switch + no tag → l2vpn_evpn skipped
- Non-default VRF (via
vrf_info["interface_vrf_mapping"]) → l2vpn_evpn never added
- Port channel member (
portchannel_info["member_mapping"]) → skipped from interface BGP loop
BGP_NEIGHBOR_AF for port channels (lines 1040–1079)
- Each
connected_portchannel → ipv4_unicast + ipv6_unicast entries
- Switch connection → l2vpn_evpn added
- Non-switch → l2vpn_evpn skipped
- Non-default VRF → l2vpn_evpn never added
BGP_NEIGHBOR for connected interfaces (lines 1081–1179)
- Direct IPv4 only → not added (excluded from BGP detection log)
- Untagged VLAN member → skipped
- No direct IPv4, no peer IP → key is
default|<port>, peer_type="external", v6only="true" (default VRF only)
- No direct IPv4, peer IP returned → key is
default|<peer_ip>, local_addr set from interface_ips or transfer_ips
- Transfer-role IPv4 →
v6only="false"
- Non-default VRF →
v6only key not present
_determine_peer_type returns "internal" (matching AS) → propagated as peer_type
BGP_NEIGHBOR for port channels (lines 1181–1229)
- Default VRF, no peer IP → key
default|<pc_name>, peer_type="external", v6only="true"
- Default VRF, peer IP → key uses peer IP;
local_addr not set (port channels don't have NetBox interface entries)
- Non-default VRF →
v6only not present
- Switch connection →
_determine_peer_type evaluated
VLAN-interface BGP (lines 1234–1339)
- VLAN with one untagged member and a peer IPv4 →
BGP_NEIGHBOR[default|<peer_ip>] and BGP_NEIGHBOR_AF[default|<peer_ip>|ipv4_unicast] added
- Same peer IP appearing on two untagged members → only one entry (deduped via
peer_ips_found)
- VLAN without
addresses / no untagged members → skipped
- No untagged member produces a peer IPv4 → warning logged, no entries added
- VLAN interface name (
f"Vlan{vid}") used to look up VRF → non-default VRF works
- Member NetBox name not in
netbox_interfaces → skipped, debug log
_get_connected_device_for_interface(device, interface_name) — config_generator.py:1342
One delegation test verifying it returns the value from get_connected_device_for_sonic_interface.
_determine_peer_type(local_device, connected_device, device_as_mapping=None) — config_generator.py:1355
Patch calculate_local_asn_from_ipv4.
- Both devices in
device_as_mapping with same AS → "internal"
- Both devices with different AS →
"external"
local_device not in mapping but has primary_ip4 → AS computed via patched function
connected_device not in mapping but has primary_ip4 → AS computed
- Either device has no
primary_ip4 and not in mapping → AS None, returns "external"
calculate_local_asn_from_ipv4 raises → caught, returns "external" (debug log)
_add_vlan_configuration(config, vlan_info, netbox_interfaces, device) — config_generator.py:1700
- VLAN with two members (one tagged, one untagged) →
VLAN[Vlan100] populated with members list (sorted by SONiC name); VLAN_MEMBER entries created with the right tagging_mode
- Default
vlanid is str(vid); name falls back to f"Vlan{vid}" if NetBox name is empty
- NetBox interface name without a SONiC mapping → warning logged, member skipped
- VLAN interface (SVI) with addresses →
VLAN_INTERFACE[Vlan<vid>] = {"admin_status": "up"} and one entry per address (VLAN_INTERFACE[Vlan<vid>|<address>] = {})
- VLAN interface without addresses → no SVI entry
_add_loopback_configuration(config, loopback_info) — config_generator.py:1760
- One Loopback with one IPv4 address →
LOOPBACK[name]={admin_status:"up"}, LOOPBACK_INTERFACE[name]={}, LOOPBACK_INTERFACE[name|addr]={}
Loopback0 with IPv4 → BGP_GLOBALS_AF_NETWORK[default|ipv4_unicast|<addr>]={}
Loopback0 with IPv6 → BGP_GLOBALS_AF_NETWORK[default|ipv6_unicast|<addr>]={}
- Non-Loopback0 →
BGP_GLOBALS_AF_NETWORK not touched
- Invalid IP string → warning logged, skipped
_get_vrf_info(device) — config_generator.py:1793
Patch get_cached_device_interfaces and convert_netbox_interface_to_sonic.
- Interface without VRF → skipped
- Interface VRF name
vrf42 (no RD) → Vrf42 definition with table_id=42
- VRF name
vrfStorage + numeric RD 2001 → vrfStorage definition with vni=2001
- VRF name
vrf2001 + textual RD vrfStorage → SONiC name vrfStorage, vni=2001
- VRF with no name match and a textual RD → uses RD as SONiC name
- VRF with no name match and no RD → warning logged, skipped
- Multiple interfaces in the same VRF → VRF defined once, both interfaces in
interface_vrf_mapping
- Per-interface exception → warning logged, processing continues
- Top-level exception (cached interfaces raise) → warning logged, returns
{"vrfs": {}, "interface_vrf_mapping": {}}
_add_vrf_configuration(config, vrf_info, netbox_interfaces) — config_generator.py:1937
- VRF with
vni → VRF (with fallback="false"), VLAN[Vlan<vni>], VLAN_INTERFACE[Vlan<vni>]={vrf_name:...}, BGP_GLOBALS_AF[<vrf>|ipv4_unicast], BGP_GLOBALS_AF[<vrf>|l2vpn_evpn] with import/export RTs and route-distinguisher, BGP_GLOBALS_ROUTE_ADVERTISE[<vrf>|L2VPN_EVPN|IPV4_UNICAST], BGP_GLOBALS_ROUTE_ADVERTISE[<vrf>|L2VPN_EVPN|IPV6_UNICAST], ROUTE_REDISTRIBUTE[<vrf>|connected|bgp|ipv4]
- VRF with
table_id only → VRF[<vrf>]={vrf_table_id: <id>}
- VRF with neither →
VRF[<vrf>]={}
BGP_GLOBALS["default"] exists → deep-copied to BGP_GLOBALS[<vrf>]
- Multiple VRFs with VNI → VXLAN_TUNNEL/VXLAN_EVPN_NVO/VXLAN_TUNNEL_MAP entries created (one map per VRF)
- No VRF with VNI → VXLAN sections not created
- Interface in
vrf_info["interface_vrf_mapping"] and in config["INTERFACE"] → vrf_name set on the interface
- Interface in
vrf_info["interface_vrf_mapping"] and in config["PORTCHANNEL_INTERFACE"] → vrf_name set on the port channel
- Interface in mapping but in neither section → debug log, no error
Mocking hints
- Build
vrf_info, vlan_info, netbox_interfaces, interface_ips, transfer_ips inline as plain dicts.
- Initialize
config with all keys the helpers mutate (BGP_NEIGHBOR, BGP_NEIGHBOR_AF, VLAN, VLAN_MEMBER, VLAN_INTERFACE, LOOPBACK, LOOPBACK_INTERFACE, BGP_GLOBALS_AF_NETWORK, VRF, VXLAN_TUNNEL, VXLAN_EVPN_NVO, VXLAN_TUNNEL_MAP, BGP_GLOBALS_AF, BGP_GLOBALS_ROUTE_ADVERTISE, INTERFACE, PORTCHANNEL_INTERFACE).
device.config_context.get(...) is consulted in _add_log_server_configuration and _add_snmp_configuration (covered in the orchestrator issue), not here.
- Construct interfaces with
SimpleNamespace and a nested vrf=SimpleNamespace(name="...", rd="...").
BGP_AF_L2VPN_EVPN_TAG comes from osism.tasks.conductor.sonic.constants — mock interfaces with tags=[SimpleNamespace(slug=BGP_AF_L2VPN_EVPN_TAG)].
Definition of Done
Dependencies
Background
Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 3 (#2199). Companion issue to the
config_generator.pyorchestrator and ports issues. Covers BGP, VLAN, Loopback, and VRF helpers inosism/tasks/conductor/sonic/config_generator.py.Scope
Add
tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.pycovering the helpers below inosism/tasks/conductor/sonic/config_generator.py.Test targets
_add_bgp_configurations(config, connected_interfaces, connected_portchannels, portchannel_info, device, device_as_mapping=None, interface_ips=None, netbox_interfaces=None, transfer_ips=None, netbox=None, vlan_info=None, vrf_info=None)—config_generator.py:914Patch
osism.tasks.conductor.sonic.config_generator.get_connected_device_for_sonic_interfaceand...get_connected_interface_ipv4_address.BGP_NEIGHBOR_AFfor connected interfaces (lines 951–1039)default|<port>|...v6only=falsesemantics deferred to BGP_NEIGHBOR test below)connected_device.role.slug∈DEFAULT_SONIC_ROLES) → l2vpn_evpn entry addedBGP_AF_L2VPN_EVPN_TAGtag → l2vpn_evpn entry addedvrf_info["interface_vrf_mapping"]) → l2vpn_evpn never addedportchannel_info["member_mapping"]) → skipped from interface BGP loopBGP_NEIGHBOR_AFfor port channels (lines 1040–1079)connected_portchannel→ ipv4_unicast + ipv6_unicast entriesBGP_NEIGHBORfor connected interfaces (lines 1081–1179)default|<port>,peer_type="external",v6only="true"(default VRF only)default|<peer_ip>,local_addrset frominterface_ipsortransfer_ipsv6only="false"v6onlykey not present_determine_peer_typereturns"internal"(matching AS) → propagated aspeer_typeBGP_NEIGHBORfor port channels (lines 1181–1229)default|<pc_name>,peer_type="external",v6only="true"local_addrnot set (port channels don't have NetBox interface entries)v6onlynot present_determine_peer_typeevaluatedVLAN-interface BGP (lines 1234–1339)
BGP_NEIGHBOR[default|<peer_ip>]andBGP_NEIGHBOR_AF[default|<peer_ip>|ipv4_unicast]addedpeer_ips_found)addresses/ no untagged members → skippedf"Vlan{vid}") used to look up VRF → non-default VRF worksnetbox_interfaces→ skipped, debug log_get_connected_device_for_interface(device, interface_name)—config_generator.py:1342One delegation test verifying it returns the value from
get_connected_device_for_sonic_interface._determine_peer_type(local_device, connected_device, device_as_mapping=None)—config_generator.py:1355Patch
calculate_local_asn_from_ipv4.device_as_mappingwith same AS →"internal""external"local_devicenot in mapping but hasprimary_ip4→ AS computed via patched functionconnected_devicenot in mapping but hasprimary_ip4→ AS computedprimary_ip4and not in mapping → ASNone, returns"external"calculate_local_asn_from_ipv4raises → caught, returns"external"(debug log)_add_vlan_configuration(config, vlan_info, netbox_interfaces, device)—config_generator.py:1700VLAN[Vlan100]populated withmemberslist (sorted by SONiC name);VLAN_MEMBERentries created with the righttagging_modevlanidisstr(vid);namefalls back tof"Vlan{vid}"if NetBox name is emptyVLAN_INTERFACE[Vlan<vid>] = {"admin_status": "up"}and one entry per address (VLAN_INTERFACE[Vlan<vid>|<address>] = {})_add_loopback_configuration(config, loopback_info)—config_generator.py:1760LOOPBACK[name]={admin_status:"up"},LOOPBACK_INTERFACE[name]={},LOOPBACK_INTERFACE[name|addr]={}Loopback0with IPv4 →BGP_GLOBALS_AF_NETWORK[default|ipv4_unicast|<addr>]={}Loopback0with IPv6 →BGP_GLOBALS_AF_NETWORK[default|ipv6_unicast|<addr>]={}BGP_GLOBALS_AF_NETWORKnot touched_get_vrf_info(device)—config_generator.py:1793Patch
get_cached_device_interfacesandconvert_netbox_interface_to_sonic.vrf42(no RD) →Vrf42definition withtable_id=42vrfStorage+ numeric RD2001→vrfStoragedefinition withvni=2001vrf2001+ textual RDvrfStorage→ SONiC namevrfStorage,vni=2001interface_vrf_mapping{"vrfs": {}, "interface_vrf_mapping": {}}_add_vrf_configuration(config, vrf_info, netbox_interfaces)—config_generator.py:1937vni→VRF(withfallback="false"),VLAN[Vlan<vni>],VLAN_INTERFACE[Vlan<vni>]={vrf_name:...},BGP_GLOBALS_AF[<vrf>|ipv4_unicast],BGP_GLOBALS_AF[<vrf>|l2vpn_evpn]with import/export RTs androute-distinguisher,BGP_GLOBALS_ROUTE_ADVERTISE[<vrf>|L2VPN_EVPN|IPV4_UNICAST],BGP_GLOBALS_ROUTE_ADVERTISE[<vrf>|L2VPN_EVPN|IPV6_UNICAST],ROUTE_REDISTRIBUTE[<vrf>|connected|bgp|ipv4]table_idonly →VRF[<vrf>]={vrf_table_id: <id>}VRF[<vrf>]={}BGP_GLOBALS["default"]exists → deep-copied toBGP_GLOBALS[<vrf>]vrf_info["interface_vrf_mapping"]and inconfig["INTERFACE"]→vrf_nameset on the interfacevrf_info["interface_vrf_mapping"]and inconfig["PORTCHANNEL_INTERFACE"]→vrf_nameset on the port channelMocking hints
vrf_info,vlan_info,netbox_interfaces,interface_ips,transfer_ipsinline as plain dicts.configwith all keys the helpers mutate (BGP_NEIGHBOR,BGP_NEIGHBOR_AF,VLAN,VLAN_MEMBER,VLAN_INTERFACE,LOOPBACK,LOOPBACK_INTERFACE,BGP_GLOBALS_AF_NETWORK,VRF,VXLAN_TUNNEL,VXLAN_EVPN_NVO,VXLAN_TUNNEL_MAP,BGP_GLOBALS_AF,BGP_GLOBALS_ROUTE_ADVERTISE,INTERFACE,PORTCHANNEL_INTERFACE).device.config_context.get(...)is consulted in_add_log_server_configurationand_add_snmp_configuration(covered in the orchestrator issue), not here.SimpleNamespaceand a nestedvrf=SimpleNamespace(name="...", rd="...").BGP_AF_L2VPN_EVPN_TAGcomes fromosism.tasks.conductor.sonic.constants— mock interfaces withtags=[SimpleNamespace(slug=BGP_AF_L2VPN_EVPN_TAG)].Definition of Done
tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.pycreatedpytest --cov=osism.tasks.conductor.sonic.config_generatorfor the targeted functions ≥ 85 %pipenv run pytest tests/unit/tasks/conductor/sonic/test_config_generator_bgp_vlan_vrf.pypasses locallyflake8,mypy,python-blackremain greenpython-osism-unit-testspassesDependencies