FabricContext: expose platform_type, retain switch records, fail closed (#399, #400) - #404
FabricContext: expose platform_type, retain switch records, fail closed (#399, #400)#404allenrobel wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances FabricContext to expose cached switch inventory details (including platform type), improves error fidelity when a fabric is missing, and hardens fabric_summary to fail closed on embedded-error payloads. These changes strengthen orchestrator pre-flight behavior and enable platform-aware feature selection without surfacing platformType to end users.
Changes:
- Add
PlatformTypeEnum, retain raw switch records inFabricContext, and exposeswitches+get_platform_type(switch_ip). - Fix missing-fabric error fidelity when the switches endpoint returns
404by raising a fabric-level “not found” message. - Harden
fabric_summaryto reject200payloads containing an embeddedcodeerror key, with unit tests/fixtures added.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
plugins/module_utils/fabric_context.py |
Adds platform-type lookup + switch record retention; improves missing-fabric error fidelity; hardens summary handling. |
plugins/module_utils/enums.py |
Introduces PlatformTypeEnum for additionalData.platformType. |
tests/unit/module_utils/test_fabric_context.py |
Adds unit tests covering embedded-error summary handling, platformType lookup, and switches-404 → fabric-not-found behavior. |
tests/unit/module_utils/fixtures/fixture_data/test_fabric_context.json |
Adds fixtures for the new tests (embedded code payload, platformType values, switches 404 + summary 404). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
plugins/module_utils/fabric_context.py:150
fabric_exists()now callsfabric_summary(), which can raiseRuntimeErrorwhen the summary payload contains an embeddedcodeerror key. The docstring currently says this method raises nothing, which is no longer accurate and can mislead callers/users of this helper.
def fabric_exists(self) -> bool:
"""
# Summary
Check whether the fabric exists (on any ND node in the cluster).
## Raises
None
"""
return self.fabric_summary is not None
plugins/module_utils/fabric_context.py:287
switch_mapcan now raise a fabric-levelRuntimeErrorfor a nonexistent fabric (via_load_switch_maps()), but its docstring only documents API-query failures. Documenting the new failure mode makes the public accessor’s contract accurate.
def switch_map(self) -> dict[str, str]:
"""
# Summary
Return a cached mapping of `fabricManagementIp` to `switchId` for all switches in the fabric.
Fetches all switches from the ND Manage Switches API on first access and caches the result.
## Raises
### RuntimeError
- If the switches API query fails.
"""
self._load_switch_maps()
|
@copilot-pull-request-reviewer Thanks — both low-confidence suggestions from the latest review are correct, and I've addressed them (these were suppressed as low-confidence, so there are no inline threads to reply on). Both
I also applied the same fix to three sibling methods that share the identical defect but weren't flagged, so the docstrings stay consistent:
Docs-only, no behavior change; |
akinross
left a comment
There was a problem hiding this comment.
slight consideration but code looks good, if not something you think we should consider it is approved from my end
991981c to
19d06a7
Compare
19d06a7 to
de4c9ca
Compare
75ec7c7 to
39417fc
Compare
Adds a str-based PlatformTypeEnum (apic/ios-xe/ios-xr/nx-os/other) mirroring the platformType enum on the fabric switches endpoint. Used to select the platform-appropriate feature model (e.g. loopback vs iosXeLoopback) without exposing the value to users. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RRzWEQiEyKV8Y8RWymhrfy
…ed (#399, #400) Three related changes to FabricContext, all in one file: - feat: retain the raw switch records from the fabric switches endpoint and add get_platform_type(switch_ip) -> PlatformTypeEnum | None (reads the nested additionalData.platformType), plus a `switches` property exposing the retained records. Enables per-switch, platform-aware model selection (loopback vs iosXeLoopback) for the loopback policy_type union work. - fix (#399): the switches endpoint returns 404 when the parent fabric is absent; _query_get swallowed that into an empty map, surfacing a misleading "switch not found" error. _load_switch_maps now confirms a 404 against fabric_summary and raises the fabric-level "fabric not found" message (shared with validate_for_mutation via _fabric_not_found_message). Not an ND deviation, so no TODO/vault marker. - hardening (#400): fabric_summary now fails closed on a 200 body carrying an embedded {"code": N, ...} error key instead of accepting it as a valid summary (which would let validate_for_mutation default open). Narrow scope (summary only), not pushed into _query_get, to avoid changing behavior for all FabricContext consumers. Also replaces the _NOT_FETCHED object() sentinel on _fabric_summary with a _fabric_summary_fetched flag so the property is correctly typed as dict | None (clears a pre-existing mypy/Pylance object-return smell on the touched code). Unit tests: +3 (embedded-code hardening, platform_type/switches, switches-404 fabric-not-found). Full module_utils suite green (3034 passed). Closes #399 Closes #400 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RRzWEQiEyKV8Y8RWymhrfy
…et_platform_type - `switches` now returns a shallow copy of the cached list so a caller mutating it cannot corrupt the cache or desync it from switch_map / switch_map_by_id. Added a test assertion pinning this. - `get_platform_type` uses a direct `PlatformTypeEnum(raw)` with try/except instead of the `raw in PlatformTypeEnum.values()` membership check, which allocated and sorted a new list on every per-switch lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RRzWEQiEyKV8Y8RWymhrfy
Address Copilot follow-up: get_platform_type accessed self.switch_map and then iterated self.switches, but the switches property returns a shallow copy (list(self._switches)) to protect the cache. That copy is an avoidable allocation on the per-switch lookup path. Call _load_switch_maps() once to populate the cache, then read self._switch_map / self._switches directly so lookups pay no copy cost and don't re-enter _load_switch_maps() via the properties. Behavior unchanged; guarded with the same is-None AssertionError pattern the switches/switch_map/switch_map_by_id properties already use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RRzWEQiEyKV8Y8RWymhrfy
…low-confidence) The embedded-code fail-closed and switches-404 fabric-not-found changes in this PR added RuntimeError paths that five docstrings' ## Raises sections did not reflect. Copilot flagged two (fabric_exists, switch_map); apply the same fix to the three siblings sharing the identical defect for consistency: - fabric_exists / fabric_is_local / fabric_is_deployment_frozen: raise via fabric_summary on an embedded `code` error payload (were "## Raises None"). - switch_map / switch_map_by_id: raise the fabric-not-found error via _load_switch_maps (documented only the API-query failure). Docs-only; no behavior change. 18 fabric_context unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GqVjUMz4tMgqrEYckf2Qqc
…he fetched flag Address @akinross review: _fabric_summary needs a tri-state (not-fetched vs fetched-and-absent) because None is a load-bearing value meaning "the fabric does not exist". This PR had used a paired _fabric_summary_fetched boolean, having dropped the previous _NOT_FETCHED = object() sentinel because object() cannot be narrowed by a type checker. Use the typed sentinel Akini suggested instead: a single-member _Sentinel enum spelled Literal[_Sentinel.UNSET] in the union, which narrows cleanly under mypy and keeps the state in one field rather than two that must be kept in sync. Adopted for the shape rather than for this one site. Today this is the only genuine tri-state in plugins/ -- every other lazy cache holds a container, where empty != absent. But if we decide FabricContext can serve as a template for future *Context classes (VrfContext, NetworkContext, etc. -- not settled), the tri-state would recur once per context class, since each would pair a summary fetch with an existence check and "does not exist" is naturally None. The paired flag would propagate a two-field sync invariant into every copy (a missed reset silently serves stale data and no type checker catches it); the sentinel makes invalidate() a single assignment that is correct by construction. Cheap enough at one site to be worth doing on the chance we go that way. _Sentinel is deliberately module-private -- promote it to a shared module if a second *Context class materializes and can inform the abstraction. Tests: +2 covering the invariant the sentinel protects, both verified to fail against a mutated implementation (invalidate resetting to None; sentinel dropped for None-means-unset): - 00180: a fetched-but-absent (None) summary is cached, so repeated fabric_exists() calls against a missing fabric do not re-query. - 00190: invalidate() clears a cached None rather than pinning it. module_utils suite green: 3036 passed. black/isort/pylint/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014aJ3Y2TJEAqJZBeUKdUnGa
…ss__
Drive-by modernization of pre-existing boilerplate in the touched test file
(not introduced by this PR), per CLAUDE.md's code standards:
- from __future__ import absolute_import, annotations, division, print_function
-> from __future__ import annotations. absolute_import/division/print_function
are no-ops on Python 3; annotations is retained per the explicit carve-out in
CLAUDE.md ("with the exception of annotations") and matches every already-
modernized test file in tests/unit/.
- __metaclass__ = type: removed (no longer needed).
No behavior change. 20 fabric_context tests green; black/isort clean; pylint
unchanged (only the known residual pytest E0401). mypy --no-incremental reports
the same two pre-existing errors before and after, with line numbers shifted by
the two removed lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aJ3Y2TJEAqJZBeUKdUnGa
get_platform_type() resolves via PlatformTypeEnum(raw) inside a try/except ValueError, falling through to None for any value the enum does not know. With SONIC absent, a SONiC switch silently reported "no platform type" rather than its actual platform -- the exact failure the enum exists to prevent. Found while checking PlatformTypeEnum against develop's pre-existing PlatformType (plugins/module_utils/models/manage_switches/enums.py), which has carried SONIC all along. Whether the two enums converge is an open question being discussed with @AKDRG on #405; this gap is a defect either way, so fix it now rather than leave it pending that outcome. If PlatformTypeEnum is later dropped in favor of a promoted PlatformType, this member goes with it. Test: extends 00230 with a fourth switch reporting platformType "sonic", asserting it resolves to PlatformTypeEnum.SONIC. Verified to fail against the unfixed enum (AttributeError: type object 'PlatformTypeEnum' has no attribute 'SONIC'). module_utils suite green: 3036 passed. black/isort/pylint/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014aJ3Y2TJEAqJZBeUKdUnGa
39417fc to
7d17d8d
Compare
Labeled
Postponedon 2026-07-16. This PR has one approval and is otherwise ready, so this note exists to stop a second approver from merging it past an unresolved design question.Why. The
PlatformTypeEnumadded here (plugins/module_utils/enums.py) duplicatesPlatformType, which already exists in develop atplugins/module_utils/models/manage_switches/enums.py— same values, except the existing one also hasSONIC. That omission is a live defect in this PR:FabricContext.get_platform_type()resolves viaPlatformTypeEnum(raw)inside atry/except ValueError, so a SONiC switch silently returnsNonerather than its platform. Two enums one token apart in the name (PlatformType/PlatformTypeEnum) is also a trap for the next reader.Proposed resolution. Promote
PlatformTypeup tomodule_utils/enums.py(per CLAUDE.md, the home for "Enums commonly used by most module utilities" — bothmanage_switchesandfabric_contextnow need it) and haveFabricContextuse it, droppingPlatformTypeEnum. A move + re-export keeps every existing import site working, so no in-flight PR needs a rebase.Blocked on.
@AKDRG's read, being discussed on #405Resolved — @AKDRG implemented the proposed promotion in #405 (approved 2026-07-22); see the 2026-07-23 update below. Original discussion: #405 (comment)The existing enums are deliberately endpoint-scoped (
PlatformType= AddSwitches POST;ShallowDiscoveryPlatformTypesplit out because that endpoint excludesapic). This PR's use is a third context — the GET switches response. If that response set can legitimately diverge from the AddSwitches request set, sharing one enum is the wrong call and a distinct, properly named read-side enum stays — withSONICadded. TheSONICgap needs fixing either way.Nothing else in this PR is affected: the
#399fidelity fix, the#400fail-closed hardening, and the_Sentinelchange all stand on their own. Remove thePostponedlabel once the enum question is settled.Update 2026-07-23 — design question settled; this is now a pure merge-ordering hold on #405. The discussion on #405 resolved in favor of the promotion: #405 (approved 2026-07-22) moves
PlatformTypeup toplugins/module_utils/enums.py.Hold this PR until #405 merges, then rebase with the following:
PlatformTypeEnumfromenums.pyand pointFabricContext.get_platform_type()at the promotedPlatformType(which includesSONIC, closing the silent-Nonedefect described above).Postponedlabel.Sequencing note for reviewers: this PR should land before the upcoming IOS-XE interface union wave (ethernet, port-channel, SVI, sub-interface — following the #403 loopback pattern). Those PRs plan to make
network_os_typeoptional via theFabricContext.get_platform_type()fallback added here.Related Issue(s)
Closes #399
Closes #400
Proposed Changes
Three related changes to
FabricContext(plugins/module_utils/fabric_context.py), plus a reusable enum:GET /api/v1/manage/fabrics/{fabric}/switchesand addget_platform_type(switch_ip) -> PlatformTypeEnum | None(reads the nestedadditionalData.platformType), plus aswitchesproperty exposing the retained records. NewPlatformTypeEnum(apic/ios-xe/ios-xr/nx-os/other) inenums.py. This lets callers select a platform-appropriate feature model (e.g.loopbackvsiosXeLoopback) without exposing the value to users — the switch-derivednetworkOSTypethe interface-granularity design calls for.404when the parent fabric is absent;_query_getswallowed that into an empty map, soget_switch_idsurfaced a misleading "switch not found"._load_switch_mapsnow confirms a404againstfabric_summaryand raises the fabric-level "fabric not found" message (shared withvalidate_for_mutationvia_fabric_not_found_message). Not an ND deviation, so noTODO(X.Y.Z)/vault marker.fabric_summarynow rejects a200body carrying an embedded{"code": N, ...}error key instead of accepting it as a valid summary (which would let everyvalidate_for_mutationcheck default open). Kept narrow (summary only; not pushed into_query_get) to avoid changing behavior for all 13FabricContextconsumers.Incidentally replaces the
_NOT_FETCHED = object()sentinel on_fabric_summarywith a typed one — a module-private single-member_Sentinelenum, spelledLiteral[_Sentinel.UNSET]in the union — so the property is correctly typeddict | None(clears a pre-existing mypy/Pylanceobject-return smell on the touched code)._fabric_summaryneeds a tri-state becauseNoneis a load-bearing value meaning "the fabric does not exist";object()couldn't be narrowed by a type checker, whereas the enum can. Keeps the state in one field rather than two that must be kept in sync, soinvalidate()is a single assignment. Per @akinross review. No behavior change.Test Notes
codehardening,platform_type/switches, switches-404→ fabric-not-found, plus two for the_Sentineltri-state: a fetched-but-absentNonesummary is cached sofabric_exists()doesn't re-query, andinvalidate()clears the cachedNonerather than pinning it), new fixtures for each._Sentineltests were verified to fail against a deliberately mutated implementation (invalidate()resetting toNone; sentinel dropped for None-means-unset), confirming they guard the invariant rather than passing vacuously.module_utilsunit suite green: 3036 passed viandpytest.black,isort,pylint,mypyclean on the changed files (nd-dev container).404on both fabric endpoints for a missing fabric;platformTypeenum values) verified against a live ND 4.2.1 lab, as documented in FabricContext: nonexistent fabric reports "switch not found" instead of "fabric not found" (nd_fabric_update_group, nd_manage_l3out) #399/FabricContext: harden fabric_summary to reject payloads carrying an embedded 'code' error key (fail closed) #400.Cisco Nexus Dashboard Version
4.2.1
Related ND API Resource Category
Checklist