Skip to content

Unit tests for osism/tasks/conductor/sonic/config_generator.py — port/interface/portchannel/breakout #2222

Description

@berendt

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 BGP/VLAN/Loopback/VRF issues. Covers the port / interface / portchannel / breakout-related helpers in osism/tasks/conductor/sonic/config_generator.py.

These helpers all mutate a config dict that is passed by reference. Tests build a small initial scaffold ({"PORT": {}, "INTERFACE": {}, "PORTCHANNEL": {}, ...}) and assert against the post-call state.

Scope

Add tests/unit/tasks/conductor/sonic/test_config_generator_ports.py covering the helpers below in osism/tasks/conductor/sonic/config_generator.py.

Test targets

_add_port_configurations(config, port_config, connected_interfaces, portchannel_info, breakout_info, netbox_interfaces, vlan_info, device)config_generator.py:314

Patch osism.tasks.conductor.sonic.config_generator.convert_sonic_interface_to_alias to return a deterministic alias, or pass a real port_config so the alias logic works end to end.

  • Sorted iteration: ports ["Ethernet0", "Ethernet4", "Ethernet120"] are processed in numeric order (verify by recording convert_sonic_interface_to_alias call sequence)
  • Master port present in breakout_cfgs → skipped (the master port is not added to config["PORT"] directly)
  • admin_status = "up" when port in connected_interfaces or in portchannel_info["member_mapping"], else "down"
  • NetBox speed (speed_explicit=True) overrides port-config speed; conversion kbps → Mbps (netbox_speed=100000port_speed="100")
  • NetBox speed with speed_explicit=False only used if port_speed is empty / "0"
  • Breakout port: speed taken from netbox_interfaces if present, otherwise derived from brkout_mode ("4x10G"10000, "4x25G"25000, "4x50G"50000, "4x100G"100000, "4x200G"200000)
  • Port index of breakout port copied from master port's index
  • Default port_data keys: admin_status, alias, index, lanes, speed, mtu="9100", adv_speeds="all", autoneg="off", link_training="off", unreliable_los="auto"
  • port_info["valid_speeds"] propagated as-is
  • No valid_speeds and port_speed set → valid_speeds = port_speed
  • Breakout port: valid_speeds is overridden via _get_breakout_port_valid_speeds
  • After main loop: calls _add_missing_breakout_ports and _add_tagged_vlans_to_ports (verify via patched mocks)

_get_breakout_port_valid_speeds(port_speed)config_generator.py:467

Pure function — quick coverage:

  • "10000""10000,1000"
  • "25000""25000,10000,1000"
  • "50000""50000,25000,10000,1000"
  • "100000""100000,50000,25000,10000,1000"
  • "200000""200000,100000,50000,25000,10000,1000"
  • "40000" (other) → "40000,10000,1000"
  • None / ""None

_calculate_breakout_port_lane(port_name, master_port, port_config)config_generator.py:489

  • Standard 4-lane: master Ethernet0 lanes "1,2,3,4", port Ethernet2"3"
  • 8-lane (400G): master Ethernet0 lanes "73,74,75,76,77,78,79,80", port Ethernet2"75,76", Ethernet6"79,80"
  • Range syntax: master lanes "1-4" → parsed via start-end branch
  • Single-lane master ("5") → defaults to lanes_per_port=1, returns single lane
  • Unexpected lane count (e.g. 6) → warning logged, lanes_per_port=1
  • Calculated range out of bounds → warning logged, returns "1" (default)
  • master_port not in port_config → returns "1"
  • Port name regex no-match → returns "1"

_add_missing_breakout_ports(config, breakout_info, port_config, connected_interfaces, portchannel_info, netbox_interfaces)config_generator.py:578

  • Breakout port already in config["PORT"] → skipped
  • Speed taken from netbox_interfaces when present (no kbps→Mbps conversion here, just str(netbox_speed) — verify the production code)
  • Speed fallback via brkout_mode ("4x25G" etc.) and ultimate default "25000"
  • admin_status follows connected_interfaces / port-channel membership
  • port_index defaults to "1", copied from master port if available
  • valid_speeds from master port's port_config[master]["valid_speeds"], then overridden by _get_breakout_port_valid_speeds(port_speed)
  • Calls convert_sonic_interface_to_alias(..., is_breakout=True, port_config=port_config) (one assertion is fine)

_add_tagged_vlans_to_ports(config, vlan_info, netbox_interfaces, device)config_generator.py:668

  • Port has multiple tagged VLANs in NetBox → config["PORT"][port]["tagged_vlans"] is the sorted list of VLAN IDs (numeric sort, e.g. ["10", "100", "20"]["10", "20", "100"])
  • Untagged VLAN members not added
  • NetBox interface name without a SONiC mapping → skipped, warning logged
  • Port present in mapping but absent from config["PORT"] → silently skipped (only existing PORT entries are updated)

_add_interface_configurations(config, connected_interfaces, portchannel_info, interface_ips, netbox_interfaces, device)config_generator.py:702

  • Connected port not in port-channel and with IPv4 in interface_ipsconfig["INTERFACE"][port] = {} and config["INTERFACE"][f"{port}|{ip}"] = {}
  • Connected port without IPv4 → config["INTERFACE"][port] = {"ipv6_use_link_local_only": "enable"}
  • Port that is a port-channel member (in portchannel_info["member_mapping"]) → skipped
  • Disconnected port → skipped
  • Port in connected_interfaces but not in netbox_interfacesnetbox_interface_name None; falls into the no-IPv4 branch

_get_transfer_role_ipv4_addresses(device)config_generator.py:747

Patch osism.tasks.conductor.sonic.config_generator.utils.nb, ...get_cached_device_interfaces.

  • transfer prefix "10.5.0.0/24", IP 10.5.0.10/24 on Eth1/1{"Eth1/1": "10.5.0.10/24"}
  • Multiple IPs on the same interface → only the first transfer-matching IP is kept (if interface.name in transfer_ips: continue)
  • IPv6 IP → ignored (only version == 4)
  • Mgmt-only or virtual interfaces → skipped (filtered out of interface_map)
  • Invalid prefix string → caught, processing continues
  • Invalid IP address → caught, processing continues
  • IP without assigned_object_id → skipped
  • IP with assigned_object_id not in interface_map → skipped
  • Top-level exception → returns {}, warning logged

_has_direct_ipv4_address(port_name, interface_ips, netbox_interfaces)config_generator.py:836

  • Port mapped, NetBox name in interface_ipsTrue
  • Port mapped, NetBox name not in interface_ipsFalse
  • Port not in netbox_interfacesFalse
  • interface_ips empty / NoneFalse
  • netbox_interfaces empty / NoneFalse

_has_transfer_role_ipv4(port_name, transfer_ips, netbox_interfaces)config_generator.py:857

Same shape as the helper above; one happy path + the empty-input early returns is enough.

_is_untagged_vlan_member(port_name, vlan_info, netbox_interfaces)config_generator.py:878

  • Port has untagged VLAN membership → True
  • Port has only tagged VLAN membership → False
  • Port not in netbox_interfacesFalse
  • vlan_info empty / NoneFalse

_add_portchannel_configuration(config, portchannel_info)config_generator.py:2078

  • One PortChannel with two members → PORTCHANNEL, PORTCHANNEL_INTERFACE (with ipv6_use_link_local_only), and per-member PORTCHANNEL_MEMBER entries created
  • Empty portchannels dict → no entries added (no exception)

Mocking hints

  • Initialize config with the keys the helpers expect to mutate. A factory fixture is helpful:
    @pytest.fixture
    def config():
        return {
            "PORT": {}, "INTERFACE": {}, "PORTCHANNEL": {}, "PORTCHANNEL_INTERFACE": {},
            "PORTCHANNEL_MEMBER": {}, "BREAKOUT_CFG": {}, "BREAKOUT_PORTS": {},
        }
  • For _add_port_configurations, patching _add_missing_breakout_ports and _add_tagged_vlans_to_ports keeps each test focused on the main-loop branches.
  • Build port_config and breakout_info inline. breakout_info is {"breakout_cfgs": {...}, "breakout_ports": {...}}.
  • netbox_interfaces shape: {sonic_name: {"speed": int|None, "speed_explicit": bool, "tags": [...], "type": str|None, "netbox_name": str}}.

Definition of Done

  • tests/unit/tasks/conductor/sonic/test_config_generator_ports.py created
  • All listed cases covered
  • pytest --cov=osism.tasks.conductor.sonic.config_generator for the targeted functions ≥ 90 %
  • pipenv run pytest tests/unit/tasks/conductor/sonic/test_config_generator_ports.py passes locally
  • flake8, mypy, python-black remain green
  • Zuul job python-osism-unit-tests passes

Dependencies

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions