Skip to content

nd_interface_loopback: policy_type discriminated union#403

Open
allenrobel wants to merge 38 commits into
developfrom
nd_interface_loopback_policy_type_union
Open

nd_interface_loopback: policy_type discriminated union#403
allenrobel wants to merge 38 commits into
developfrom
nd_interface_loopback_policy_type_union

Conversation

@allenrobel

@allenrobel allenrobel commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Formerly stacked on #387 (nd_argument_specs_relocate). #387 has merged, this PR is retargeted to develop and rebased on it, and remote CI now runs (and passes) on this branch.

⚠️ Merge Ordering — hold until #422 merges

Do not merge this PR before #422 (replaced/overridden field-removal detection). Reasoning:

Rebase checklist for this branch after #422 lands:

  • Move the "routeMapTag": "12345" entry into NexusLoopbackPolicyModel.reverse_diff_defaults; keep adminState at the level where it lives post-restructure.
  • Add reverse_diff_defaults tables for the eight new policy models (ipfmLoopback, mplsLoopback, and the six IOS-XE templates), each sourced from its policyType's OpenAPI template schema; lab-verify echoes where possible. Any policy model without a table silently loses replaced/overridden removal detection.
  • Verify the read-tolerant policy models do not retain unknown server-injected keys — any retained extra key would be counted as a removal by the reverse pass and break replaced/overridden idempotency (the 2026-07-18 XE probe found no ND-injected keys in XE policy reads, but re-check on the rebased code).

Related Issue(s)

Relates to #401 (develop ip=IPv4CIDR handling). The bare-IPv4 normalization on this branch overlaps with open PR #402 — whichever lands second will rebase and drop the duplicate change. No issue is closed by this PR.

The design/plan docs that accompanied the POC phase have been removed from the branch and archived outside the repo.

Proposed Changes

Consolidates the loopback interface behind discriminated unions so a single module handles every ND loopback policy template, across both switch OSes.

policy_type discriminated union (NX-OS):

  • Add LoopbackPolicyTypeEnum and a LoopbackPolicyBase; make policy_type required and strict on the write path.
  • Add IpfmLoopbackPolicyModel, MplsLoopbackPolicyModel, and SecondaryIpModel; wire the discriminated union on policy_type.
  • Extend the loopback argspec with policy_type and the per-template union fields.
  • query_all manages loopback, ipfmLoopback, and mplsLoopback via a set filter.
  • Read path tolerates ND-injected policy keys (extra="forbid" was aborting reads); the write path stays strict.
  • Normalize loopback ip to bare IPv4 (ND 4.2.1 rejects CIDR on the wire).
  • Docs for policy_type and the per-template fields.

Required network_os_type discriminator:

IOS-XE loopback surface (choices: [nx-os, ios-xe]):

  • config_data.network_os is an outer discriminated union (NexusLoopbackNetworkOSModel | XeLoopbackNetworkOSModel), each OS branch carrying its own policy_type union: NX-OS keeps loopback/ipfmLoopback/mplsLoopback; IOS-XE adds iosXeLoopback, iosXeLoopbackShutNoshut, iosXeUnderlayLoopback, iosXeInternalLoopback, csrLoopback, csr1kvLoopback (all six real XE templates; userDefined stays excluded on both OSes). Cross-OS mismatches (nx-os + iosXeLoopback) fail structurally in Pydantic — no hand-written cross-check.
  • LoopbackPolicyStrictBase split: extra="forbid" + read-tolerant validator + admin_state shared by all nine branches; NX common fields stay on LoopbackPolicyBase.
  • Six IOS-XE branch models with exact per-template fields (schema-verified); new flat options secondary_ip, enable_pim.
  • query_all filter is the union of the NX and XE policy-type enums (XeLoopbackPolicyTypeEnum).
  • Lab probe (2026-07-18, IOS-XE device in fabric ISN) proved the OpenAPI READ schema's csrIntLoopback never appears on the wire — ND echoes the create-side csrLoopback — so the model uses the single wire-verified name (drift recorded in the team bug-tracker vault: csr-loopback-read-schema-name-drift); the same probe found no ND-injected keys in XE policy reads.
  • DOCUMENTATION/EXAMPLES cover all four states for ios-xe plus a csrLoopback example.
  • XE integration tasks gated on nd_test_xe_switch_ip (skipped unless supplied), including a fabric-equality-guarded mixed NX+XE overridden variant.

Lab-surfaced bug fixes:

  • create_bulk now groups one POST per (switch, policyType) — ND rejects mixed-policyType bulk creates (207 with a failed item, nothing created) — and both create paths validate 207 results[] per item instead of trusting the HTTP code (stopgap until Detect Multi-status per-item failures in NdV1Strategy (#295) #398's NdV1Strategy 207 support; both carry TODO(4.2.1) markers referencing vault notes: bulk-interface-create-rejects-mixed-policy-types and multi-status-207-status-field-inconsistent).
  • Integration loopback IDs renumbered 1xx→2xx: the lab's new multisite topology auto-provisions loopback100 (ND's default multisite loopback ID) on border gateways, colliding with the old reserved range.

Test Notes

  • Integration: nd_interface_loopback integration tests are green against these changes (merged/replaced/overridden/query, including the added policy_type: loopback tasks).
  • Unit: all unit tests green — models (test_loopback_interface.py), orchestrator, and nd_argument_specs suites.
  • 2026-07-16 (network_os_type commits): integration tests re-run green against a live ND 4.2.1 lab with the now-required network_os_type: nx-os in every network_os block; full unit suite (3195 tests) green; validate-modules sanity clean on nd_interface_loopback.py.
  • 2026-07-18 (IOS-XE commits): full unit suite (3208 tests) green; validate-modules clean; black/isort/pylint/mypy clean on changed files; ansible-lint clean on the integration target. Live-lab raw-REST probe verified XE create/read/delete behavior on a real IOS-XE device (intent-only). XE integration run against the live lab: all tests pass (all four states + idempotency on the IOS-XE device, iosXeLoopback and csrLoopback), which also verifies the csr single-name decision on the full deploy path — the deployed read echoes csrLoopback, not the read-schema's csrIntLoopback. The first run surfaced two real module bugs (mixed-policyType bulk-create rejection masked by unchecked 207s) — fixed with TDD unit coverage, then the re-run went green. (No CI on stacked PRs — local verification per team convention.)

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

@allenrobel allenrobel self-assigned this Jul 15, 2026
@allenrobel
allenrobel force-pushed the nd_argument_specs_relocate branch from 67358f0 to 2afa387 Compare July 16, 2026 00:46
@allenrobel allenrobel changed the title [POC] nd_interface_loopback: policy_type discriminated union nd_interface_loopback: policy_type discriminated union Jul 19, 2026
@allenrobel
allenrobel marked this pull request as ready for review July 19, 2026 17:47
@allenrobel allenrobel added the ready for review Submitter is requesting a PR review label Jul 19, 2026
Base automatically changed from nd_argument_specs_relocate to develop July 19, 2026 18:04
@allenrobel
allenrobel force-pushed the nd_interface_loopback_policy_type_union branch from e4da480 to 78cb3f2 Compare July 19, 2026 18:17
Comment thread plugins/module_utils/models/interfaces/loopback_interface.py
Comment thread plugins/module_utils/orchestrators/loopback_interface.py
Comment thread plugins/module_utils/orchestrators/loopback_interface.py Outdated
Comment thread plugins/module_utils/orchestrators/loopback_interface.py Outdated
Comment thread plugins/module_utils/orchestrators/loopback_interface.py Outdated
Comment thread plugins/module_utils/orchestrators/loopback_interface.py
Comment thread plugins/module_utils/orchestrators/loopback_interface.py
Comment thread plugins/module_utils/orchestrators/loopback_interface.py
allenrobel added a commit that referenced this pull request Jul 21, 2026
…lasses (#403)

Review feedback from @akinross:

- Rename LoopbackPolicyBase/LoopbackPolicyModel to NexusLoopbackPolicyBase/
  NexusLoopbackPolicyModel so NX-OS-specific classes carry an OS prefix like
  the Xe* classes do (Nexus chosen to match NexusLoopbackNetworkOSModel).
  Ipfm/Mpls/Csr* classes keep their names - they already mirror their wire
  policyType discriminator, which is OS-unambiguous.
- Expose policy_type as a property on LoopbackInterfaceModel; drop the
  orchestrator's getattr-chain _extract_policy_type static method.
- Extract bulk-create group construction into
  _group_by_switch_and_policy_type (the mixed-policy-type TODO marker moves
  with it).
- Replace the group tuples with frozen slotted dataclasses
  _BulkCreateGroupKey/_BulkCreateItem for readability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHGFDqqU37PRqNHhKw1FCQ
Copilot AI review requested due to automatic review settings July 21, 2026 16:07

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 expands nd_interface_loopback from a single NX-OS loopback template into a discriminated-union driven implementation that supports multiple NX-OS loopback policy templates and a full IOS-XE loopback surface, while hardening create/bulk-create behavior against ND’s per-item 207 Multi-Status failure reporting and policyType-mixing limitations.

Changes:

  • Introduces required discriminators network_os_type (outer union) and policy_type (policy union) and adds per-template models/argspec/docs to support NX-OS + IOS-XE loopback templates in one module.
  • Normalizes loopback IPv4 ip input to a bare host address for ND 4.2.1 wire compatibility (accepts CIDR input, sends bare IPv4).
  • Updates the orchestrator to (a) validate per-item DATA.results[] statuses for create/create_bulk and (b) group bulk creates by (switch_id, policy_type).

Reviewed changes

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

Show a summary per file
File Description
tests/unit/module_utils/orchestrators/test_loopback_interface.py Updates orchestrator unit tests for new model types, adds coverage for per-item 207 failures and bulk-create grouping by policy type, and expands query_all filtering expectations.
tests/unit/module_utils/models/test_loopback_interface.py Adds extensive unit coverage for the new discriminated unions (NX + XE) and for IPv4 host normalization + strict/read-tolerant policy validation.
tests/unit/module_utils/fixtures/fixture_data/test_loopback_interface.json Extends fixtures to cover new query_all filtering cases and the new 207-results failure shapes used in create/create_bulk tests.
tests/integration/targets/nd_interface_loopback/vars/main.yaml Updates reserved loopback ID ranges, makes network_os_type/policy_type explicit, and adds IOS-XE test variables gated by nd_test_xe_switch_ip.
tests/integration/targets/nd_interface_loopback/tasks/main.yaml Conditionally includes IOS-XE integration tasks when nd_test_xe_switch_ip is provided.
tests/integration/targets/nd_interface_loopback/tasks/setup.yaml Renumbers reserved loopback IDs used for integration setup cleanup.
tests/integration/targets/nd_interface_loopback/tasks/merged.yaml Updates merged-state integration coverage to include required discriminators and new ID ranges.
tests/integration/targets/nd_interface_loopback/tasks/replaced.yaml Updates replaced-state tasks for required discriminators and renumbered IDs.
tests/integration/targets/nd_interface_loopback/tasks/overridden.yaml Updates overridden-state tasks for required discriminators, renumbered IDs, and system-loopback filtering assumptions.
tests/integration/targets/nd_interface_loopback/tasks/deleted.yaml Updates deleted-state tasks for renumbered IDs and the revised vars structure.
tests/integration/targets/nd_interface_loopback/tasks/xe.yaml Adds IOS-XE integration coverage across all states plus idempotency, including mixed NX+XE overridden coverage when fabrics match.
plugins/modules/nd_interface_loopback.py Updates module docs/examples to expose and describe the new discriminators and per-template policy fields across NX-OS and IOS-XE.
plugins/module_utils/orchestrators/loopback_interface.py Hardens create/create_bulk against per-item failures and changes bulk-create grouping to (switch_id, policy_type); expands query_all filtering to NX + XE managed policy types.
plugins/module_utils/models/types.py Adds IPv4Host type to normalize IPv4 to bare host form while accepting CIDR input.
plugins/module_utils/models/interfaces/loopback_interface.py Reworks loopback models into nested discriminated unions (network OS + policy type), adds strict/read-tolerant behavior for policy objects, and exposes required discriminators via argspec.
plugins/module_utils/models/interfaces/enums.py Adds managed loopback policy-type enums for NX-OS and IOS-XE used by orchestrator filtering and documentation.

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

Comment thread plugins/modules/nd_interface_loopback.py
@allenrobel
allenrobel requested a review from akinross July 21, 2026 16:21
allenrobel added a commit that referenced this pull request Jul 22, 2026
Copilot review: the ip option description did not mention that CIDR
input (e.g. 10.1.1.1/32) is accepted but normalized to the bare address
on the wire and in returned state (IPv4Host, models/types.py).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqAuV2pWYJTno2bfdcNZCm
allenrobel and others added 5 commits July 22, 2026 06:11
… pilot

Records the ND 4.2.1 interface-tree analysis behind the loopback pilot: cut
modules at (interfaceType x mode), express templates as a typed policy_type
discriminated union, and derive networkOSType from the switch rather than
forking modules or asking the user. Loopback is the pilot for both the union
pattern (policy_type: loopback | ipfm | mpls) and unfreezing network_os_type
to support IOS-XE (iosXeLoopback).

Exploratory/POC branch stacked on nd_argument_specs_relocate (#384). Known
unresolved dependencies before implementation: #384 in review, generated
argspec (decision 2), and FabricContext not yet exposing switch OS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNFbfgLeEKXi5g25NmJTSS
Records the design for the nd_interface_loopback policy_type union pilot:
shared LoopbackPolicyBase + three branch subclasses (loopback, ipfmLoopback,
mplsLoopback) behind a Pydantic discriminated union on policy_type, with
set-based query_all filtering via a new LoopbackPolicyTypeEnum.

Two decisions locked in with the maintainer: no backward compatibility
(modules unreleased, so policy_type is required with no default), and strict
per-branch validation via extra="forbid" + a None-stripping before-validator
so wrong-branch fields hard-fail while unset flat-argspec options do not.
network_os_type stays frozen (IOS-XE is a separate, FabricContext-blocked
follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNFbfgLeEKXi5g25NmJTSS
Making policy_type required breaks the existing nd_interface_loopback
integration tasks, which omit it. Spec now covers adding policy_type: loopback
to all 9 existing policy blocks (vars/main.yaml, replaced/overridden/merged
tasks) to preserve current coverage; ipfm/mpls integration cases deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNFbfgLeEKXi5g25NmJTSS
TDD, bite-sized task plan (9 tasks) implementing the discriminated-union
spec: LoopbackPolicyTypeEnum, shared base + three branch models, the
discriminated union field, flat argspec with required policy_type, set-based
query_all filtering, module docs, and the integration-task policy_type
additions. Each task ends with an independently testable deliverable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNFbfgLeEKXi5g25NmJTSS
allenrobel and others added 27 commits July 22, 2026 06:11
Replace the hardcoded policyType == "loopback" filter in
LoopbackInterfaceOrchestrator.query_all with a set-membership check
against LoopbackPolicyTypeEnum so query_all returns all three
policy-typed loopback templates the module now manages. Corrects the
query_all and module-level docstrings, which previously claimed
ipfmLoopback would be handled by a dedicated module.

Also fixes _build_loopback_model() in the orchestrator test file,
which omitted the now-required policy_type discriminator and broke
~9 tests once policy_type became required.
…ture formatting

- Add missing ## Test section to test_loopback_interface_00760 docstring
- Remove redundant inline `from pydantic import ValidationError` in two model
  tests that already have the module-level import
- Normalize test_loopback_interface_00760a/b/c fixture formatting to match
  surrounding one-key-per-line style
…read path

LoopbackPolicyBase.strip_none_valued_keys now also strips keys not declared on
the model when validation context is {"mode": "read"} (set by
LoopbackInterfaceModel.from_response). This tolerates ND-injected read-only
policy keys (e.g. linkStateRoutingTag, confirmed live on ND 4.2.1) that
previously tripped extra="forbid" and aborted the module on every read.
Write-side input (no read context) remains strict, so wrong-branch fields are
still rejected.

Also fixes a stale unit test (test_loopback_interface_00420) whose premise
predated the ipfmLoopback/mplsLoopback discriminated-union support already on
this branch: policyType="ipfmLoopback" is a supported value, not a rejected
one; only a value outside the three-way union should still fail from_response.
Live ND 4.2.1 400s a loopback create when ip is CIDR (e.g. 10.99.99.250/32)
and accepts the bare form. Add IPv4Host to models/types.py (BeforeValidator
normalizing IPv4Interface to its bare .ip form) and switch
LoopbackPolicyBase.ip from IPv4CIDR to IPv4Host so bare or CIDR input is
accepted but always normalized to bare before it reaches the wire; this also
keeps idempotency since ND always returns bare on read. ipv6 (IPv6CIDR) is
unchanged - not verified against ND in this probe.

Updated unit test SAMPLE data/assertions that expected the CIDR ip form to
expect the normalized bare form; integration tasks under
tests/integration/targets/nd_interface_loopback already use bare ip values
and needed no changes.
Per team decision, the platform/OS selector is a mandatory argspec
parameter for now so interface parameters validate before any
controller call; a FabricContext.get_platform_type() fallback comes
later when the parameter becomes optional. Mirrors the ND schema,
where networkOSType is a required create-side discriminator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aJ3Y2TJEAqJZBeUKdUnGa
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ypeEnum

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-18)

ND 4.2.1 echoes csrLoopback on reads despite the OpenAPI read schema
listing csrIntLoopback; drop the defensive dual-name tolerance. Drift
recorded in the bug-tracker vault (csr-loopback-read-schema-name-drift).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cleanups

- 00770c: nest networkOSType inside networkOS; policy objects mirror the
  2026-07-18 lab GET key set; honest TEST_NOTES
- XeLoopbackNetworkOSModel: restore not-frozen rationale comment
- 00770: assert result length, not just the policy-type set
- argspec: secondary_ip/enable_pim placed to match DOCUMENTATION order
- integration: XE gate rejects empty nd_test_xe_switch_ip
- csr1kv extra_config description aligned with siblings
- query_all: hoist invariant managed_policy_types out of the switch loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…back100)

The lab's new multisite topology (S1_BG1 <-> WAN1/ISN) auto-provisions
loopback100 (policyType multisiteLoopback, ND's default multisite loopback
ID) on the border gateway. query_all rightly filters system-provisioned
types, so the module cannot see the name squatter and the create 500s
("loopback100 is already in use"). Move the reserved test range clear of
ND defaults: IDs 200-209 (+299 never-exists probe), IPs 10.100.20x.x.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ND rejects mixed-policyType bulk creates (207 with a failed item and
nothing created) and the module trusted the 207, reporting changed while
creating nothing - surfaced by the XE integration idempotency test.
Group one POST per (switch, policyType) and fail on any results[] item
whose status is not exactly 'success'. Scoped to the loopback
orchestrator; PR #398's NdV1Strategy 207 support supersedes the check.
Vault: bulk-interface-create-rejects-mixed-policy-types,
multi-status-207-status-field-inconsistent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The in-repo design and plan docs were POC-phase working artifacts; per the
PR description's graduation note they are removed before opening for team
review. Archived locally at ~/pr/403 for historical reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lasses (#403)

Review feedback from @akinross:

- Rename LoopbackPolicyBase/LoopbackPolicyModel to NexusLoopbackPolicyBase/
  NexusLoopbackPolicyModel so NX-OS-specific classes carry an OS prefix like
  the Xe* classes do (Nexus chosen to match NexusLoopbackNetworkOSModel).
  Ipfm/Mpls/Csr* classes keep their names - they already mirror their wire
  policyType discriminator, which is OS-unambiguous.
- Expose policy_type as a property on LoopbackInterfaceModel; drop the
  orchestrator's getattr-chain _extract_policy_type static method.
- Extract bulk-create group construction into
  _group_by_switch_and_policy_type (the mixed-policy-type TODO marker moves
  with it).
- Replace the group tuples with frozen slotted dataclasses
  _BulkCreateGroupKey/_BulkCreateItem for readability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHGFDqqU37PRqNHhKw1FCQ
Copilot review: the ip option description did not mention that CIDR
input (e.g. 10.1.1.1/32) is accepted but normalized to the bare address
on the wire and in returned state (IPv4Host, models/types.py).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqAuV2pWYJTno2bfdcNZCm
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

Development

Successfully merging this pull request may close these issues.

4 participants