Skip to content

FabricContext: expose platform_type, retain switch records, fail closed (#399, #400) - #404

Open
allenrobel wants to merge 8 commits into
developfrom
fabric_context_platform_type
Open

FabricContext: expose platform_type, retain switch records, fail closed (#399, #400)#404
allenrobel wants to merge 8 commits into
developfrom
fabric_context_platform_type

Conversation

@allenrobel

@allenrobel allenrobel commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto develop after #405 (2026-08-27) — hold lifted

This PR was labeled Postponed on 2026-07-16 because the PlatformTypeEnum it introduced duplicated the existing PlatformType
(then in models/manage_switches/enums.py), and its copy also omitted SONIC, so FabricContext.get_platform_type() silently
returned None for a SONiC switch. The design discussion on #405 settled on promoting PlatformType to plugins/module_utils/enums.py,
which #405 did (merged 2026-08-25).

Done on this rebase:

  • Dropped this PR's PlatformTypeEnum from enums.py; FabricContext.get_platform_type() now returns the promoted
    PlatformType (which includes SONIC, closing the silent-None defect) — commit "FabricContext: use the promoted PlatformType,
    drop PlatformTypeEnum". PlatformType.normalize() is deliberately not used on the read path: its None -> NX_OS default would
    mask a switch that reports no platformType, which must stay None.
  • Removed the Postponed label.

enums.py is no longer touched by this PR. Everything else (#399 fidelity fix, #400 fail-closed hardening, _Sentinel tri-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_type optional via the
FabricContext.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):

  • feat — expose switch platform type. Retain the raw switch records from GET /api/v1/manage/fabrics/{fabric}/switches and add get_platform_type(switch_ip) -> PlatformType | None (reads the nested additionalData.platformType), plus a switches property exposing the retained records. Uses the PlatformType enum Enhancing Fabric Type Support for Switches Module #405 promoted to plugins/module_utils/enums.py. This lets callers select a platform-appropriate feature model (e.g. loopback vs iosXeLoopback) without exposing the value to users — the switch-derived networkOSType the interface-granularity design calls for.
  • fix FabricContext: nonexistent fabric reports "switch not found" instead of "fabric not found" (nd_fabric_update_group, nd_manage_l3out) #399 — fabric-not-found fidelity. The switches endpoint returns 404 when the parent fabric is absent; _query_get swallowed that into an empty map, so get_switch_id surfaced a misleading "switch not found". _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(X.Y.Z)/vault marker.
  • hardening FabricContext: harden fabric_summary to reject payloads carrying an embedded 'code' error key (fail closed) #400 — fail closed. fabric_summary now rejects a 200 body carrying an embedded {"code": N, ...} error key instead of accepting it as a valid summary (which would let every validate_for_mutation check default open). Kept narrow (summary only; not pushed into _query_get) to avoid changing behavior for all 13 FabricContext consumers.

Incidentally replaces the _NOT_FETCHED = object() sentinel on _fabric_summary with a typed one — a module-private single-member _Sentinel enum, spelled Literal[_Sentinel.UNSET] in the union — so the property is correctly typed dict | None (clears a pre-existing mypy/Pylance object-return smell on the touched code). _fabric_summary needs a tri-state because None is 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, so invalidate() is a single assignment. Per @akinross review. No behavior change.

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

Copilot AI review requested due to automatic review settings July 15, 2026 21:17
@allenrobel allenrobel added the ready for review Submitter is requesting a PR review label Jul 15, 2026
@allenrobel allenrobel self-assigned this Jul 15, 2026
@allenrobel allenrobel removed the ready for review Submitter is requesting a PR review label Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 in FabricContext, and expose switches + get_platform_type(switch_ip).
  • Fix missing-fabric error fidelity when the switches endpoint returns 404 by raising a fabric-level “not found” message.
  • Harden fabric_summary to reject 200 payloads containing an embedded code error 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.

Comment thread plugins/module_utils/fabric_context.py Outdated
Comment thread plugins/module_utils/fabric_context.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread plugins/module_utils/fabric_context.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 calls fabric_summary(), which can raise RuntimeError when the summary payload contains an embedded code error 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_map can now raise a fabric-level RuntimeError for 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()

@allenrobel

Copy link
Copy Markdown
Collaborator Author

@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 ## Raises sections were stale after this PR's new RuntimeError paths:

  • fabric_exists() — now raises via fabric_summary on an embedded code error payload; docstring said "None".
  • switch_map — now raises the fabric-not-found error via _load_switch_maps; docstring documented only the API-query failure.

I also applied the same fix to three sibling methods that share the identical defect but weren't flagged, so the docstrings stay consistent:

  • fabric_is_local and fabric_is_deployment_frozen (same fabric_summary raise path as fabric_exists).
  • switch_map_by_id (same _load_switch_maps raise path as switch_map).

Docs-only, no behavior change; fabric_context unit tests remain green. Fixed in commit "FabricContext: document new RuntimeError paths in ## Raises (Copilot low-confidence)".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@allenrobel allenrobel added the ready for review Submitter is requesting a PR review label Jul 16, 2026
akinross
akinross previously approved these changes Jul 16, 2026

@akinross akinross left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

slight consideration but code looks good, if not something you think we should consider it is approved from my end

Comment thread plugins/module_utils/fabric_context.py Outdated
@allenrobel allenrobel added postponed and removed ready for review Submitter is requesting a PR review labels Jul 16, 2026
@allenrobel
allenrobel force-pushed the fabric_context_platform_type branch 2 times, most recently from 991981c to 19d06a7 Compare July 22, 2026 16:12
@allenrobel
allenrobel force-pushed the fabric_context_platform_type branch from 19d06a7 to de4c9ca Compare July 27, 2026 23:18
@allenrobel
allenrobel force-pushed the fabric_context_platform_type branch 5 times, most recently from 39417fc to 7d17d8d Compare August 13, 2026 19:25
@allenrobel
allenrobel force-pushed the fabric_context_platform_type branch 4 times, most recently from 48ab3e3 to 9a373f2 Compare August 27, 2026 20:37
@allenrobel allenrobel added ready for review Submitter is requesting a PR review and removed postponed labels Aug 27, 2026
@allenrobel
allenrobel requested a review from akinross August 27, 2026 20:43
allenrobel and others added 8 commits August 28, 2026 07:42
…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.
@allenrobel
allenrobel force-pushed the fabric_context_platform_type branch from 9a373f2 to 0c0fcf1 Compare August 28, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Submitter is requesting a PR review

Projects

None yet

4 participants