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
39417fc to
7d17d8d
Compare
48ab3e3 to
9a373f2
Compare
…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
…type The enums.py hunk this commit originally carried (adding SONIC to PlatformTypeEnum) was dropped on rebase: develop's PlatformType (promoted by #405) already includes SONIC. The fixture and assertion guarding the silent-None fall-through remain.
#405 promoted PlatformType (nx-os/other/ios-xe/ios-xr/sonic/apic) to module_utils/enums.py, which is the enum FabricContext.get_platform_type() was always meant to share. Point the read path at it and remove the duplicate read-side PlatformTypeEnum this PR had introduced. normalize() is deliberately not used here: its None -> NX_OS default would mask a switch that reports no platformType, which must stay None.
9a373f2 to
0c0fcf1
Compare
Rebased onto develop after #405 (2026-08-27) — hold lifted
This PR was labeled
Postponedon 2026-07-16 because thePlatformTypeEnumit introduced duplicated the existingPlatformType(then in
models/manage_switches/enums.py), and its copy also omittedSONIC, soFabricContext.get_platform_type()silentlyreturned
Nonefor a SONiC switch. The design discussion on #405 settled on promotingPlatformTypetoplugins/module_utils/enums.py,which #405 did (merged 2026-08-25).
Done on this rebase:
PlatformTypeEnumfromenums.py;FabricContext.get_platform_type()now returns the promotedPlatformType(which includesSONIC, closing the silent-Nonedefect) — commit "FabricContext: use the promoted PlatformType,drop PlatformTypeEnum".
PlatformType.normalize()is deliberately not used on the read path: itsNone -> NX_OSdefault wouldmask a switch that reports no
platformType, which must stayNone.Postponedlabel.enums.pyis no longer touched by this PR. Everything else (#399fidelity fix,#400fail-closed hardening,_Sentineltri-state)is unchanged since @akinross's approval.
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):GET /api/v1/manage/fabrics/{fabric}/switchesand addget_platform_type(switch_ip) -> PlatformType | None(reads the nestedadditionalData.platformType), plus aswitchesproperty exposing the retained records. Uses thePlatformTypeenum Enhancing Fabric Type Support for Switches Module #405 promoted toplugins/module_utils/enums.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).test_fabric_context.py20 passed;black,isort,pylint,mypyclean onfabric_context.py/test_fabric_context.py(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