Skip to content

Unit tests for osism/tasks/conductor/sonic/interface.py — detect_breakout_ports & detect_port_channels #2220

@berendt

Description

@berendt

Background

Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 3 (#2199). Companion to the interface-conversion issue: covers the two large topology-detection functions in osism/tasks/conductor/sonic/interface.py. They are split out because each is several hundred lines of NetBox-driven loop logic with multiple branches (standard breakout, 400G breakout, LAG name regex variants).

Scope

Add tests/unit/tasks/conductor/sonic/test_interface_detection.py covering detect_breakout_ports and detect_port_channels in osism/tasks/conductor/sonic/interface.py.

Test targets

detect_breakout_ports(device)interface.py:603

Patch osism.tasks.conductor.sonic.interface.get_cached_device_interfaces and the local get_port_config. Build interfaces with types.SimpleNamespace; type values via SimpleNamespace(value="...").

Early exits / error paths

  • Device without custom_fields["sonic_parameters"]["hwsku"] → returns {"breakout_cfgs": {}, "breakout_ports": {}}, warning logged
  • get_port_config returns {} → returns empty result
  • get_port_config raises → returns empty result
  • get_cached_device_interfaces raises → returns empty result, warning logged

NetBox-format breakout (Eth1/49/1..4)

  • Single subport in group → not a breakout, no entries
  • 4-subport 25G group → breakout_cfgs[master_port] = {brkout_mode: "4x25G", port: "1/49", breakout_owner: "MANUAL"}, four breakout_ports[Ethernet*] entries with master
  • 4-subport 50G group → brkout_mode: "4x50G"
  • 4-subport 100G group → brkout_mode: "4x100G"
  • 4-subport 200G group → brkout_mode: "4x200G"
  • 4-subport 10G group → brkout_mode: "4x10G"
  • Speed not on interface.speed but resolvable via interface.type.value → still works
  • Unsupported speed (e.g. 40000, 4 subports) → group skipped (continue), no entries
  • Master port has lanes="1,2,3,4,5,6,7,8" (8 lanes) → offset multiplier 2, breakout port numbers go Ethernet0/2/4/6 not Ethernet0/1/2/3
  • Same group key (module/port) appears twice in interface list → only processed once (processed_groups)

SONiC-format 400G breakout (8-lane master)

  • Ethernet0,2,4,6 all at 100000 Mbps with master Ethernet0 having 8 lanes → detected as 4x100G, port = "1/1"
  • Same for Ethernet8,10,12,14 (master Ethernet8 with 8 lanes) → port = "1/2"
  • 8-lane master but interfaces not at 100000 → not detected
  • Master Ethernet0 with only 4 lanes → not treated as 400G; falls through to standard pattern

SONiC-format standard breakout (Ethernet0..3)

  • Four consecutive interfaces at 25000 Mbps → 4x25G
  • Four consecutive interfaces at 50000 Mbps → 4x50G
  • Four interfaces at 100000 Mbps (not breakout — too fast) → skipped (≤ 50G filter)
  • Three interfaces only → not a group
  • Speed derived from interface.type.value → still works

detect_port_channels(device)interface.py:950

Patch osism.tasks.conductor.sonic.interface.get_cached_device_interfaces and convert_netbox_interface_to_sonic (or inject HWSKU + port_config so the conversion works without further mocking).

Early exits

  • get_cached_device_interfaces raises → returns {"portchannels": {}, "member_mapping": {}}, warning logged
  • No LAG interfaces and no interface.lag → returns empty dicts

LAG name regex variants (re.match against lag_parent.name)

  • "PortChannel1"PortChannel1
  • "Port-Channel2"PortChannel2
  • "LAG3"PortChannel3
  • "ae4"PortChannel4
  • "bond5"PortChannel5
  • "po-uplink-7" (number anywhere) → PortChannel7 (numeric fallback)
  • "trunk" (no number) and the LAG is in lag_interfaces at index 0 → PortChannel1
  • "trunk" and the LAG is not in lag_interfacesPortChannel1 (string "1" fallback)
  • Mixed case ("portchannel99", "PORT-CHANNEL10") → matched case-insensitively

Members & ordering

  • One LAG with two members → members is sorted by numeric suffix (Ethernet120, Ethernet124["Ethernet120", "Ethernet124"])
  • Same SONiC name added twice → de-duplicated (membership check before append)
  • Default PortChannel config dict: admin_status="up", fast_rate="true", min_links="1", mtu="9100"
  • member_mapping populated (interface name → port channel name)

Mocking hints

  • Build interfaces inline:
    iface = SimpleNamespace(
        name="Eth1/49/1",
        id=1,
        mgmt_only=False,
        type=SimpleNamespace(value="100gbase-x-qsfp28"),
        speed=100000,
    )
  • For LAG members, set iface.lag = SimpleNamespace(name="PortChannel1", id=99).
  • port_config for detect_breakout_ports must contain the master port (Ethernet0 etc.) with the right lanes string. Build it inline.
  • For SONiC-format breakout tests, the interfaces list contains SimpleNamespace(name="Ethernet0", ...) etc.; the production code matches them via re.match(r"Ethernet(\d+)", ...).
  • Cover both code paths in processed_groups for the SONiC 400G branch (continue on success).

Definition of Done

  • tests/unit/tasks/conductor/sonic/test_interface_detection.py created
  • All listed cases covered
  • pytest --cov=osism.tasks.conductor.sonic.interface for the two target functions ≥ 90 %
  • pipenv run pytest tests/unit/tasks/conductor/sonic/test_interface_detection.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