Skip to content

Enhancing Fabric Type Support for Switches Module - #405

Open
AKDRG wants to merge 10 commits into
CiscoDevNet:developfrom
AKDRG:nd_switches_enhancement
Open

Enhancing Fabric Type Support for Switches Module#405
AKDRG wants to merge 10 commits into
CiscoDevNet:developfrom
AKDRG:nd_switches_enhancement

Conversation

@AKDRG

@AKDRG AKDRG commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Proposed Changes

This PR improves nd_manage_switches support for fabric-specific switch onboarding by adding fabric capability validation, exposing supported switch platform types, and improving lifecycle output visibility for config-save and deploy operations.

Changes

  • Added fabric capability validation before switch discovery/add workflows.
  • Enforced fabric-specific support for:
  1. switch role
  2. preserve_config
  3. platform_type
  • Exposed platform_type in the playbook argspec with supported choices:
  1. nx-os
  2. ios-xe
  3. ios-xr
  4. other
  • Registered finalize operations in module output:
  1. config_save
  2. deploy_switches
  3. deploy_config
  • Preserved full controller response metadata for finalize API calls where available.
  • Added/updated unit tests for fabric validation, platform input handling, and finalize output visibility.

Test Notes

Validated against live controller
pytest tests/unit/module_utils/test_nd_switch_resources.py -q
44 passed

Cisco Nexus Dashboard Version

4.2(1)

Related ND API Resource Category

  • analyze
  • infa
  • 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

Comment thread plugins/module_utils/manage_switches/fabric_switch_capabilities.py
@allenrobel

Copy link
Copy Markdown
Collaborator

@AKDRG — a heads-up and a question, not a request to change anything here. This PR is doing the right thing; the collision is on my side.

Situation. In #404 I added a PlatformTypeEnum to plugins/module_utils/enums.py for FabricContext.get_platform_type(), which reads additionalData.platformType off GET /fabrics/{fabric}/switches. I did that without noticing your PlatformType in models/manage_switches/enums.py already covers the same ground — same values, except yours also has SONIC, which mine is missing (a real gap: my lookup would silently return None for a SONiC switch). Two enums one token apart in the name is a trap for the next reader, so I'd rather converge than ship the duplicate.

Proposal. Promote PlatformType from models/manage_switches/enums.py up to module_utils/enums.py, and have FabricContext use it. Per CLAUDE.md, module_utils/enums.py is for "Enums commonly used by most module utilities" — with both manage_switches and fabric_context needing it, that's what it's become. The reverse (a top-level util importing from models/manage_switches/) would be a layering inversion.

This shouldn't cost you anything. Done as a move + re-export — leaving PlatformType re-exported from models/manage_switches/enums.py (via __all__, to keep pylint quiet) — every existing import site keeps working untouched, nothing in this PR changes, and you don't need to rebase. module_utils/enums.py imports only from enum import Enum, so there's no cycle, and the re-export lives in a named module rather than __init__.py, so it's clear of the empty-init sanity rule. Two of your changed files (nd_switch_resources.py, config_models.py) are PlatformType consumers, which is exactly why I'd rather not touch import sites while you're in flight.

The question I actually want your read on. Your enums look deliberately endpoint-scoped — PlatformType documented as AddSwitches (POST switches), ShallowDiscoveryPlatformType split out because shallowDiscovery excludes apic. My use is a third context: the GET switches response. Is that response set known to match the AddSwitches request set, or would you rather it stay a separate enum on principle? If they can legitimately diverge, sharing one is the wrong call and I'll keep a distinct read-side enum — properly named and documented, with SONIC added. You know this API surface better than I do.

One detail either way: normalize() defaults None -> NX_OS, which suits the write path but not mine, where "switch reports no platformType" has to stay None. So I'd use the bare enum rather than normalize() regardless.

Happy to do the move in #404 if you're good with it, or leave it to you if you'd rather own that file.

allenrobel added a commit that referenced this pull request Jul 16, 2026
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
allenrobel added a commit that referenced this pull request Jul 19, 2026
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
@allenrobel

Copy link
Copy Markdown
Collaborator

@AKDRG thanks for moving PlatformType to plugins/module_utils/enums.py 👍

I'll wait for #405 to merge to leverage PlatformType at its new home.

@allenrobel allenrobel 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.

Code review

Four findings, each as an inline comment: endpoint-import coupling in shared utils.py, role: None rejected by the new fabric-capability validation, a now-dead all_preserve_config flag, and an undocumented ValueError in normalize_platform_type.

🤖 Generated with Claude Code

Comment thread plugins/module_utils/utils.py Outdated
Comment thread plugins/module_utils/manage_switches/fabric_capabilities.py Outdated
Comment thread plugins/module_utils/manage_switches/nd_switch_resources.py
Comment thread plugins/module_utils/models/manage_switches/config_models.py
@AKDRG AKDRG added the ready for review Submitter is requesting a PR review label Jul 22, 2026
allenrobel added a commit that referenced this pull request Jul 22, 2026
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
allenrobel
allenrobel previously approved these changes Jul 23, 2026

@allenrobel allenrobel 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.

LGTM after comments were addressed.

@AKDRG
AKDRG requested a review from akinross July 23, 2026 14:47
@sivakasi-cisco sivakasi-cisco added the nac01 NaC ND release 0.0.1 label Jul 23, 2026
akinross
akinross previously approved these changes Jul 24, 2026
@sivakasi-cisco

Copy link
Copy Markdown
Collaborator

#438

allenrobel added a commit that referenced this pull request Jul 27, 2026
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
Comment thread plugins/module_utils/manage_switches/nd_switch_resources.py
if self.state not in ("merged", "replaced", "overridden"):
self.nd.module.fail_json(msg=f"Unsupported state: {self.state}")

# --- Validate & classify ------------------------------------------------

@mikewiebe mikewiebe Jul 28, 2026

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.

High: Onboarding-only preserve_config validation rejects existing no-op switches

Issue

Capability validation runs before compute_changes(), so it validates preserve_config for every desired switch, including switches already managed and ultimately classified as idempotent. Because the model injects preserve_config: false, an existing External switch fails unless every reconciliation playbook repeats the historical onboarding choice.

Evidence

  • nd_switch_resources.py lines 3042-3059 runs the new capability validation before diff classification and idempotency.

  • config_models.py lines 323-327 defaults omitted preserve_config to false.

  • fabric_switch_capabilities.py lines 243-247 rejects that value unconditionally for External fabrics.

  • nd_switch_resources.py lines 2748-2756 also hard-codes preserve_config: false in gathered inventory.

  • An exact-head probe classified an existing External switch as a complete no-op, with every work bucket empty, but the pre-classification capability check still raised:

    preserve_config 'false' is not supported for External ...
    Supported preserve_config values: true.
    
  • For example, an External switch is initially onboarded with preserve_config: true. On a later idempotency run, the playbook describes the same managed switch but omits this onboarding-only option:

    - cisco.nd.nd_manage_switches:
        state: merged
        config:
          - ip_address: 192.0.2.20
            username: admin
            password: password

    The model inserts preserve_config: false, and capability validation rejects it before compute_changes() can establish that the operation is a no-op:

    preserve_config 'false' is not supported for External.
    Supported preserve_config values: true.
    

    Had change computation run first, every work bucket would have been empty:

    switches_to_add: []
    switches_to_update: []
    switches_to_remove: []
    

    Gathered External configurations have the same problem because the serializer explicitly emits preserve_config: false.

Existing PR overlap

No matching existing PR comment found. Earlier comments discuss a dead preserve-config wait flag but do not cover pre-diff validation of an onboarding-only setting.

Existing open issue overlap

No matching open issue found. Focused searches for External switch and preserve_config idempotency did not identify an owner.

Impact

Existing External switch playbooks that were idempotent before this PR can now fail before diffing, discovery, or writes. Gathered External configuration is inherently unreplayable because it carries the rejected default.

Suggested fix

Classify the plan before enforcing onboarding-only fields and validate preserve_config only for switches that will actually be discovered, added, or re-added. Preserve input explicitness if omission must differ from false. Add a regression test for an existing External no-op switch with omitted preserve_config.


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.

Can you clarify why External switches require preserve_config: true? Is this an ND controller requirement or an intended module policy?

preserve_config appears to control initial onboarding behavior. Requiring it to remain true during every subsequent reconciliation means an omitted value—and the module’s own gathered value of false—causes an already-managed no-op switch to fail.

If this is a controller requirement, could we document the rationale and limit its validation to discovery, add, and re-add operations? Otherwise, should omission be accepted for switches that are already managed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

External Fabric has fixed preserve config "true". Omission has been now handled through the default validator which will fix it to true.

@mikewiebe mikewiebe Aug 21, 2026

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.

Following are 3 more issues introduced after the last set of changes

1. High: Omitted platform defaults still break an existing Campus IOS-XE no-op

Issue

@AKDRG while verifying this update, found the following regession/behavior. Are you planning to handle this as part of #488?

Capability validation applies the NX-OS add default before determining whether an existing switch needs onboarding work. An idempotent Campus IOS-XE leaf therefore fails when a normal reconciliation playbook omits the newly introduced platform_type.

This is a regression introduced by PR #405. Before this PR, platform_type was not part of the module's public switch configuration, and current develop treats the same input as an unchanged no-op.

Evidence

  • nd_switch_resources.py lines 3051-3067 validates capabilities before compute_changes() can classify the existing switch as idempotent.

  • fabric_switch_capabilities.py lines 283-299 changes an omitted platform to NX-OS and validates the role against that assumed platform.

  • config_models.py lines 283-294 excludes platform_type from normal-switch diffing, confirming it is not a drift field for an existing no-op.

  • Practical example: assume Campus_AK already contains this switch:

    Management IP: 192.0.2.10
    Platform:      ios-xe
    Role:          leaf
    

    A pre-PR reconciliation playbook can legitimately omit the newly introduced platform field:

    - name: Reconcile the existing Campus IOS-XE leaf
      cisco.nd.nd_manage_switches:
        fabric: Campus_AK
        state: merged
        config:
          - seed_ip: 192.0.2.10
            username: admin
            password: "{{ switch_password }}"
            role: leaf
            # platform_type omitted

    Expected:

    changed: false

    Actual result under PR Enhancing Fabric Type Support for Switches Module #405:

    role 'leaf' is not supported for platform_type 'nx-os'
    in Campus VXLAN
    
  • The exact-head diff engine correctly classified this input as:

    idempotent=1
    to_add=0
    to_update=0
    

    The actual module never reaches that result because capability validation runs first.

  • Current develop at 3026bddc147861e07aa89b26be1277f884c6def4 treats the same input as a no-op. The regression exists at both the previously reviewed PR head 7fbfa7c and current head d0e213c.

Existing PR overlap

This is a follow-up to the existing pre-diff validation thread. Omitted preserve_config derivation was addressed, but this confirms that onboarding-only defaults are still applied to existing idempotent switches.

Existing open issue overlap

Related to #488, which says NX-OS should be the default only for add operations. It does not defer this PR-specific existing-switch regression.

Impact

A valid Campus IOS-XE leaf that previously returned changed: false now fails before discovery, diffing, or writes. Adding platform_type: ios-xe avoids the failure, but requiring newly introduced onboarding metadata in every existing-switch reconciliation is the compatibility regression.

Suggested fix

Classify entries against live inventory before applying onboarding defaults. For an existing switch, resolve an omitted platform from its inventory record; apply the NX-OS default only to actual add or re-add operations. Add a manage_state() regression test proving this Campus IOS-XE input performs no writes.

@mikewiebe mikewiebe Aug 21, 2026

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.

2. High: The derived iBGP preserve default lets POAP and swap finish before reboot

Issue

preserve_config no longer has one static default. When omitted, PR #405 derives true for Data Center VXLAN iBGP and false for eBGP and Campus. The iBGP value is applied to every operation, including POAP and swap.

Those operations are consequently placed in the preserve-config wait set, which can accept the first pre-reboot normal response instead of requiring the documented normal -> unreachable -> ok reboot transition.

Evidence

  • fabric_switch_capabilities.py lines 113-118 sets the omitted iBGP value to true.
  • fabric_switch_capabilities.py lines 283-300 applies it without considering whether the operation is normal onboarding, POAP, or swap.
  • The corresponding omitted defaults are false for eBGP and Campus. Changing every fabric default to false would route this omitted-value example through the reboot wait, but it would also change normal iBGP brownfield onboarding and would not protect an explicit preserve_config: true POAP/swap input.
  • nd_switch_resources.py lines 1546-1562 places every NX-OS entry with preserve_config: true in nxos_preserve.
  • utils.py lines 792-807 explains that POAP devices must be observed through the reboot transition, but lines 863-866 accept a preserve-config device as soon as systemMode == normal.

For example:

- name: Bootstrap an iBGP leaf
  cisco.nd.nd_manage_switches:
    fabric: FABRIC_IBGP
    state: merged
    config:
      - seed_ip: 192.0.2.20
        username: admin
        password: "{{ switch_password }}"
        role: leaf
        poap:
          serial_number: FDO12345ABC
          hostname: leaf1
        # preserve_config omitted

PR #405 derives:

preserve_config=True
nxos_reload=[]
nxos_preserve=['FDO12345ABC']

Immediately after import, ND can still return the pre-reboot state:

systemMode=normal
discoveryStatus=ok

The preserve-config branch accepts that first response:

wait_returned=True
calls=['fetch']

Expected behavior is to keep waiting until the reboot transition has been observed:

normal -> unreachable -> ok

Existing PR overlap

Related to the resolved wait-set discussion and the active preserve-config thread. Neither discussion tests the derived iBGP default through POAP or swap readiness.

Existing open issue overlap

Related to #488, which owns fabric-derived preserve defaults. It does not cover operation-specific POAP/swap reboot behavior.

Impact

The module can proceed with credentials, save, deploy, and report success while the switch has not completed—or even begun—its expected reboot. Subsequent finalization may fail or act against a switch that is not ready.

Suggested fix

Make readiness operation-aware instead of changing every fabric default. Normal onboarding can retain its fabric-derived value, while NX-OS POAP and swap imports should always use the reload-observation policy regardless of preserve_config.

If preserve mode is not meaningful for those operations, reject or ignore it explicitly. Add omitted-value and explicit-true regression tests that require an observed unreachable -> ok transition before finalization.

@mikewiebe mikewiebe Aug 21, 2026

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.

3. High: No switches can be added to External and Inter-Fabric Connectivity fabrics

Issue

PR #405’s new fabric exposure gate excludes the fabric type shown in ND as External and Inter-Fabric Connectivity (externalConnectivity).

Consequently, cisco.nd.nd_manage_switches rejects every switch addition to this fabric type before discovery or any write. This affects NX-OS, IOS-XE, IOS-XR, and other platforms.

This is a regression: current develop allows the request to proceed to the controller, while PR #405 rejects it locally.

Practical example

- name: Add an NX-OS switch to External and Inter-Fabric Connectivity
  cisco.nd.nd_manage_switches:
    fabric: EXTERNAL_A
    state: merged
    config:
      - seed_ip: 192.0.2.30
        username: admin
        password: "{{ switch_password }}"
        platform_type: nx-os
        role: leaf

PR #405 fails with the equivalent of:

Fabric type 'externalConnectivity' is not supported.

Changing platform_type does not help because the fabric type—not the switch platform—is rejected.

Impact

No switches can be added to External and Inter-Fabric Connectivity fabrics through cisco.nd.nd_manage_switches. Read-only gathering still works, and legacy cisco.dcnm.dcnm_inventory or direct API workflows are unaffected.

Suggested fix

Add normalized externalConnectivity to the supported-fabric gate and test onboarding with all four supported platform types. This is also explicitly required by open issue #488.

Comment thread plugins/modules/nd_manage_switches.py
Comment thread plugins/modules/nd_manage_switches.py Outdated
Comment thread plugins/modules/nd_manage_switches.py Outdated
Comment thread plugins/modules/nd_manage_switches.py Outdated
Comment thread plugins/modules/nd_manage_switches.py Outdated
Comment thread plugins/modules/nd_manage_switches.py Outdated
allenrobel added a commit that referenced this pull request Aug 10, 2026
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
allenrobel added a commit that referenced this pull request Aug 11, 2026
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
allenrobel added a commit that referenced this pull request Aug 11, 2026
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
allenrobel added a commit that referenced this pull request Aug 11, 2026
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

@allenrobel allenrobel 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.

Code review

Incremental review of the three commits since my last review (Fix Preserve Config, Platform Type Validation + Support Docs, Remove support for IPFM/Tier2, Refine switch preserve config capabilities). One finding, posted inline — a follow-up to the earlier (resolved) IPFM thread.

🤖 Generated with Claude Code

Comment on lines +173 to +178
family="IPFM",
fabric_types=frozenset({"ipfm", "ipfmenhanced"}),
platform_types=frozenset({PlatformType.NX_OS}),
roles=IPFM_ROLES,
preserve_config_values=frozenset({False}),
default_preserve_config=False,

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.

Follow-up to the earlier IPFM thread ("We shouldn't expose any support for IPFM yet" / "Removed."): the removal in Remove support for IPFM/Tier2 covers the tier2_leaf argspec choice, the DOCUMENTATION notes, and the unit test, but this capability entry still makes IPFM fully functional. Nothing gates the fabric parameter, so a task targeting an ipfm/ipfmenhanced fabric with role: leaf or spine still validates and onboards end-to-end. IPFM_ROLES (line 93) also still contains SwitchRole.TIER2_LEAF, so the enforcement layer continues to accept the role that was removed from the argspec. If IPFM should not be exposed yet, this entry (and IPFM_ROLES) needs to be removed or gated as well; if the entry stays, the DOCUMENTATION notes should re-document IPFM's constraints, including the newly added default_preserve_config=False derivation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gated the onboarding of switches for supported fabrics through SUPPORTED_SWITCH_ONBOARDING_FABRIC_TYPES. Documentation has been modified to announce the support of only DC VXLAN and Campus.

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.

High: Unsupported fabrics still reach destructive removal paths

Issue

The new supported-fabric gate runs after the early deleted and empty-overridden branches. Those states can therefore remove selected switches—or the entire inventory—from IPFM or any other fabric the module declares unsupported.

Evidence

For example, the module correctly rejects a nonempty write configuration against IPFM:

- cisco.nd.nd_manage_switches:
    fabric: IPFM_A
    state: merged
    config:
      - seed_ip: 192.0.2.10
        username: admin
        password: "{{ switch_password }}"
Fabric type 'ipfm' is not supported.

However, changing only the state to deleted bypasses that gate:

- name: Remove one switch from the unsupported IPFM fabric
  cisco.nd.nd_manage_switches:
    fabric: IPFM_A
    state: deleted
    config:
      - seed_ip: 192.0.2.10

The exact-head probe called the bulk-remove path with that switch's serial number.

More seriously, an empty overridden configuration also bypasses validation:

- name: Remove every switch from the unsupported IPFM fabric
  cisco.nd.nd_manage_switches:
    fabric: IPFM_A
    state: overridden
    config: []

That path calls _handle_deleted_state(None), which treats the whole existing inventory as the deletion target.

Existing PR overlap

This expands the active IPFM fail-closed thread. The latest change blocks onboarding, but destructive states remain reachable because they return before the new validation call.

Existing open issue overlap

Tracked by #488, which requires fabric validation before write operations. #477 is also related because it requires explicit validation of switch-removal safety.

Impact

A module advertised as unsupported for IPFM can still remove a selected IPFM switch. Empty overridden can deboard every switch in the fabric, making the fail-closed boundary inconsistent specifically on destructive operations.

Suggested fix

Resolve and validate the fabric support boundary before every mutating state, including deleted and empty overridden.

Keep onboarding-field validation separate so deletion does not require credentials, role, platform, or preserve settings. If deboarding from unsupported fabrics is intentionally allowed, document and test that narrower contract explicitly instead of bypassing the gate implicitly.

allenrobel added a commit that referenced this pull request Aug 13, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

nac01 NaC ND release 0.0.1 ready for review Submitter is requesting a PR review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants