Skip to content

interface capability preflight (#273)#275

Open
allenrobel wants to merge 12 commits into
developfrom
nd_interface_capability_preflight
Open

interface capability preflight (#273)#275
allenrobel wants to merge 12 commits into
developfrom
nd_interface_capability_preflight

Conversation

@allenrobel
Copy link
Copy Markdown
Collaborator

@allenrobel allenrobel commented May 11, 2026

Related Issue(s)

Closes #273, #300, #301, #302

Proposed Changes

Pre-flights ND's unpublished /api/v1/manage/fabrics/{fabric}/capableSwitches endpoint before per-switch configuration calls, converting confusing HTTP 400s on incapable switches into a single aggregate error that names the offending switches.

  • Endpoint model EpManageFabricsCapableSwitchesGet, inheriting _EpManageFabricsBase for path construction, with required interfaceType / mode query params.
  • InterfaceCapabilityPreflight class (plugins/module_utils/interface_capability_preflight.py) mirroring FabricContext shape:
    • (interface_type, mode) cache (switch records + derived switch-id set); one GET per pair per task run
    • Client-side taxonomy enforcement (captured from the ND 4.2 GUI 2026-05-11) — invalid combos raise ValueError without consuming a round trip
    • Optional FabricContext injection enriches error messages with switch_ip
    • invalidate() for cache reset
    • Treats HTTP 404 from capableSwitches as a hard failure (rather than caching an empty capable set that would mislabel every requested switch); returns defensive copies from get_capable_switches / get_capable_switch_ids so callers can't corrupt cached state.
  • Orchestrator wiring on NDBaseInterfaceOrchestrator:
  • Loopback as reference implinterface_type = "loopback", interface_mode = "managed"; preflight invoked from create, update, create_bulk. Delete and read paths intentionally skip preflight.

Taxonomy embedded in InterfaceCapabilityPreflight docstring

interface_type valid modes
loopback managed
svi managed
tunnel managed
ethernet trunk, access, routed, fex, pvlan, dot1qTunnel, unmanaged
portChannel trunk, access, routed, fex, pvlan, dot1qTunnel, unmanaged

VPC uses a different endpoint (/vpcPairs?view=intendedPairs) and is handled separately (follow-up). Breakout and subinterface are out of scope (no pre-check available / absent from taxonomy).

Impact on the merged nd_interface_loopback module

nd_interface_loopback is already merged to develop, so this PR is not purely additive — it modifies files owned by that merged module to wire loopback in as the reference implementation:

  • plugins/module_utils/orchestrators/loopback_interface.py — adds the interface_type / interface_mode ClassVars and the validate_switches_capable calls in create / update / create_bulk.
  • tests/unit/module_utils/orchestrators/test_loopback_interface.py — each mutating test now consumes one capableSwitches GET.
  • tests/unit/module_utils/fixtures/fixture_data/test_loopback_interface.json — adds the capableSwitches fixture responses.

No change to plugins/modules/nd_interface_loopback.py, the loopback model, or loopback's user-facing behavior — preflight is internal and runs ahead of the existing API calls. These three files are the "modifies already-merged code" surface for review; anyone with an in-flight branch off loopback should expect a small merge touch-point here.

Code-review follow-ups resolved in this PR

A high-effort /code-review pass on this branch surfaced three additional findings whose fixes touch files originating in develop (base_interface.py, loopback_interface.py, both from PR #220). All three are now resolved in this PR rather than deferred:

The two in-branch findings — 404 silently cached as empty capable set, and cache returned by reference — are fixed in interface_capability_preflight.py (commit 7a130c4).

Adopting preflight in downstream interface modules

Once this lands, each new interface orchestrator (svi, ethernet access / trunk-host, port-channel, ...) opts in with no boilerplate beyond two ClassVars:

  1. Inherit NDBaseInterfaceOrchestrator — already standard for interface orchestrators; nothing new.

  2. Set both ClassVars to a valid taxonomy pair from the table above:

    • interface_type: ClassVar[str] = "..." (e.g. "ethernet", "portChannel", "svi")
    • interface_mode: ClassVar[str] = "..." (e.g. "access", "trunk", "routed", "managed")

    One orchestrator = one (interface_type, interface_mode) pair; per-mode variants (e.g. ethernet access vs. trunk-host) each set their own interface_mode.

  3. Call validate_switches_capable(...) at the top of create, update, and create_bulk, before resolving switch IDs or building payloads — [model_instance] for the single-instance methods, the full list for create_bulk. Skip it in delete and query paths (matching loopback).

  4. Nothing else to override. _resolve_mode() no longer exists — mode is the interface_mode ClassVar. If a subclass sets interface_type but omits interface_mode, validate_switches_capable raises a clear RuntimeError naming the class.

  5. Opt out by leaving interface_type as "" (the base default) — for interface types with no capableSwitches coverage (breakout, subinterface).

  6. VPC is separate — vpc orchestrators use a different endpoint (/vpcPairs?view=intendedPairs) and must not set these ClassVars; vpc preflight is a follow-up.

  7. Tests — every mutating test gains one capableSwitches GET response immediately after the switches-list response; test_loopback_interface.json shows the fixture shape.

Test Notes

Cisco Nexus Dashboard Version

4.2.1

Related ND API Resource Category

  • analyze
  • infra
  • manage
  • onemanage
  • other

Checklist

  • Latest commit is rebased from develop with merge conflicts resolved
  • New or updates to documentation has been made accordingly
  • Assigned the proper reviewers

🤖 Generated with Claude Code

@allenrobel allenrobel self-assigned this May 12, 2026
@allenrobel allenrobel linked an issue May 12, 2026 that may be closed by this pull request
6 tasks
@allenrobel allenrobel force-pushed the nd_interface_capability_preflight branch 2 times, most recently from 05d6436 to 6d822a3 Compare May 14, 2026 20:43
@allenrobel allenrobel changed the title Add interface capability preflight (#273) interface capability preflight (#273) May 18, 2026
@allenrobel allenrobel force-pushed the nd_interface_loopback branch from bff5d88 to 921294d Compare May 20, 2026 21:23
@allenrobel allenrobel force-pushed the nd_interface_capability_preflight branch from 6d822a3 to 7ca333a Compare May 20, 2026 21:23
Base automatically changed from nd_interface_loopback to develop May 21, 2026 13:25
@allenrobel allenrobel force-pushed the nd_interface_capability_preflight branch from 7ca333a to 930a991 Compare May 21, 2026 19:41
Copilot AI review requested due to automatic review settings May 21, 2026 21:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an interface-capability “preflight” layer to the collection so interface orchestrators can query ND’s unpublished .../capableSwitches endpoint up-front and fail with a single aggregated, switch-specific error instead of scattered per-switch HTTP 400s.

Changes:

  • Introduces a new endpoint model for GET /api/v1/manage/fabrics/{fabric}/capableSwitches with query params (interfaceType, mode) and unit tests.
  • Adds InterfaceCapabilityPreflight (taxonomy enforcement + per-(interface_type, mode) caching + validation/error aggregation) with fixture-backed unit tests.
  • Wires capability preflight into NDBaseInterfaceOrchestrator and enables it for the loopback orchestrator (create/update/create_bulk).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plugins/module_utils/interface_capability_preflight.py Implements cached capability lookup + taxonomy enforcement + aggregate validation errors.
plugins/module_utils/endpoints/v1/manage/manage_fabric_capable_switches.py Adds endpoint model for the unpublished capableSwitches resource.
plugins/module_utils/orchestrators/base_interface.py Adds orchestrator-level preflight plumbing (interface_type, lazy preflight, grouping/aggregation).
plugins/module_utils/orchestrators/loopback_interface.py Opts loopback orchestrator into preflight and calls it on create/update/bulk create.
tests/unit/module_utils/test_interface_capability_preflight.py Unit tests for caching, taxonomy enforcement, and validation behavior.
tests/unit/module_utils/fixtures/fixture_data/test_interface_capability_preflight.json Fixture responses for preflight unit tests.
tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_capable_switches.py Unit tests for new endpoint params/path behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/module_utils/orchestrators/loopback_interface.py
Comment thread plugins/module_utils/orchestrators/base_interface.py Outdated
@allenrobel allenrobel force-pushed the nd_interface_capability_preflight branch from a2f288a to 509cc8c Compare May 21, 2026 22:19
allenrobel and others added 8 commits May 28, 2026 07:33
Pre-flights ND's unpublished `/api/v1/manage/fabrics/{fabric}/capableSwitches`
endpoint so we can fail fast with a single aggregate error naming offending
switches instead of letting per-switch configuration calls return confusing
HTTP 400s.

- `EpManageFabricsCapableSwitchesGet` endpoint model with required
  `interfaceType` / `mode` query params
- `InterfaceCapabilityPreflight` class with `(interface_type, mode)` cache,
  client-side taxonomy enforcement, and optional `FabricContext`-driven
  error enrichment
- `NDBaseInterfaceOrchestrator.validate_switches_capable()` groups model
  instances by `(interface_type, mode)`, aggregates per-group errors into
  one `RuntimeError`
- Loopback orchestrator opts in via `interface_type: ClassVar[str] = "loopback"`
  and calls the preflight from `create`, `update`, `create_bulk`
- 42 new unit tests (9 endpoint, 33 preflight) including 24 parametrized
  taxonomy cases captured from the ND 4.2 GUI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `Optional[str]` with `str | None` per CLAUDE.md modernization rule
for new code; the legacy form leaked in from mirroring manage_fabrics.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
get_capable_switch_ids rebuilt the {switchId} set on every call. validate()
runs once per interface, so for N interfaces the comprehension ran N times
over the full fabric switch list (O(N*S)). Cache the derived set per
(interface_type, mode) alongside the existing record cache, and clear it in
invalidate() so the two caches stay consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EpManageFabricsCapableSwitchesGet hand-rolled set_identifiers, the
fabric_name check, and BasePath path-building -- all already provided by
_EpManageFabricsBase, the same base used by the sibling
EpManageFabricsSummaryGet. The class already declared the base-only
_require_fabric_name / _path_suffix ClassVars, confirming the intent.

Inherit _EpManageFabricsBase, drop the duplicated set_identifiers and
_require_fabric_name, and reduce the path override to query-param
validation plus a super().path delegation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
validate() hand-formatted the capableSwitches URL as an f-string,
duplicating what the endpoint model already produces and risking drift if
the path or query encoding ever changes. Add a _build_endpoint() helper as
the single source of truth for the URL, used by both get_capable_switches()
(live request) and validate() (the "Verify via GET ..." error hint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The class, __init__, and validate docstrings claimed error messages were
enriched with switch_ip / switch_name / model. The code only adds switch_ip
(FabricContext exposes no switch-name/model map). Correct all three to
describe what the code actually does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- get_capable_switches: drop the dead `if result else []` ternary;
  _query_get always returns a dict so `result.get("switches") or []` suffices.
- validate: add a WHY comment explaining the soft switch_map_by_id.get()
  lookup (FabricContext.get_switch_ip() raises on miss).
- _resolve_mode: add a comment explaining why getattr() is used -- ModelType
  is bound to NDBaseModel, which has no `mode`; the attribute is duck-typed.
- base_interface: import Iterable from collections.abc per PEP 585.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`mode` is a per-interface-type constant (a frozen Literal nested under
config_data) on every interface model -- loopback, svi, ethernet-access --
not per-instance data. The base _resolve_mode() default
getattr(model_instance, "mode") matched zero real models and would raise
AttributeError for any opted-in orchestrator whose model lacked a
top-level `mode`.

Promote `mode` to an `interface_mode` ClassVar alongside `interface_type`.
validate_switches_capable() reads it directly and raises a clear
RuntimeError when an orchestrator sets interface_type without
interface_mode, enforcing the opt-in contract structurally rather than by
docstring. The (interface_type, mode) grouping is dropped -- it only
existed to handle a per-instance mode.

Add base-level tests for validate_switches_capable (opted-out no-op;
half-configured error); it had no coverage before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@allenrobel allenrobel force-pushed the nd_interface_capability_preflight branch from 806a39a to 4e4a362 Compare May 28, 2026 17:34
allenrobel and others added 4 commits May 29, 2026 14:02
Treat HTTP 404 from the capableSwitches endpoint as a hard failure rather
than silently caching an empty capable set, which would mislabel every
requested switch as "not capable". Return defensive copies from
get_capable_switches/get_capable_switch_ids so callers can't corrupt
cached state. See #273.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bulk callers (e.g. LoopbackInterfaceOrchestrator.create_bulk) iterate
the same parameter a second time to build per-switch payload groups
after validate_switches_capable has already walked it once. The
Iterable[ModelType] signature openly invited a future caller to pass
a generator/filter/map, which would exhaust the iterator on the first
pass and silently turn the bulk create into a no-op POST chain while
the state machine still reports changed=True.

Narrow the parameter to Sequence[ModelType] so mypy catches generator
callers at the call boundary. Today's only caller passes list[...],
so behavior is unchanged.

Fixes #300

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`validate_switches_capable` resolved every `switch_ip` inside a single set
comprehension, so the first unknown IP short-circuited the whole preflight
before any capability check ran. A typo on one entry hid resolution and
capability problems on the remaining entries, forcing a round-trip-per-error
workflow.

Resolve all `switch_ip` values up front, collect the unresolvable ones, and
raise a single aggregate RuntimeError naming every unknown IP. The capability
GET still runs on the resolved subset only after the resolution check passes.

Fixes #301.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`validate_switches_capable` issues a live GET against the unpublished
`capableSwitches` endpoint. `RestSend.commit()` only short-circuits writes in
`--check` mode, so this GET runs in dry-runs too. Once the 404-aware error
handling treats endpoint failures as hard errors, a `--check` run on an ND
release that has retired or relocated the endpoint will fail a dry-run that
previously succeeded.

In `--check` mode, catch `RuntimeError` from `capability_preflight.validate(...)`
and downgrade it to a warning via `AnsibleModule.warn`. Endpoint outages and
capability mismatches surface as informational output instead of failing the
dry-run. Outside check mode, behavior is unchanged. Unresolvable `switch_ip`
values still raise even in check mode because they reflect user input errors.

Add `warn()` to `MockAnsibleModule` so tests can assert on emitted warnings.

Fixes #302.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants