Skip to content

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

@berendt

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

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions