Skip to content

nd_interface_subinterface_managed: add the IOS-XE iosXeSubinterface and iosXeSubinterfaceShutNoshut policy types - #572

Open
allenrobel wants to merge 1 commit into
feat_540_svi_iosxefrom
feat_541_subinterface_iosxe
Open

allenrobel wants to merge 1 commit into
feat_540_svi_iosxefrom
feat_541_subinterface_iosxe

Conversation

@allenrobel

Copy link
Copy Markdown
Collaborator

Related Issue(s)

Closes #541

Stacked on #571 (feat_540_svi_iosxe); refs #409 (grouped bulk create), #564 (4.3.1 create-required template defaults).

Proposed Changes

Extends nd_interface_subinterface_managed with the IOS-XE branch on the discriminated-union pattern of #558 / #570 / #571.

Applicability (ND 4.2.1 and 4.3.1 OpenAPI, identical enum and template fields on both):

policyType Template Classification
iosXeSubinterface ios_xe_int_subintf Supported: adminState, description (1-200), extraConfig, ip, prefix (8-31), ipv6, ipv6Prefix (64-127), vlanId (1-4094), vrfInterface
iosXeSubinterfaceShutNoshut ios_xe_int_subif_admin_state Supported: adminState only
iosXeInternalSubinterface ios_xe_int_subif_internal Not applicable (ND-internal, like the NX-OS subinterfaceInternal)
userDefined n/a Out of scope (as in every interface module)
  • config_data.network_os is a union on network_os_type (injected as nx-os when omitted, existing playbooks unchanged); the NX-OS policy moves onto InterfacePolicyStrictBase, so cross-platform fields (mtu, routing tag, PIM, Netflow on IOS-XE) fail before any controller call.
  • IOS-XE branch: policy_type union of XeSubinterfacePolicyModel (injected as iosXeSubinterface) and XeSubinterfaceShutNoshutPolicyModel, with the IOS-XE template ranges.
  • interface_name accepts the Catalyst parent families: an abbreviated / lowercase prefix that matches exactly one canonical name (Ethernet, Port-channel, GigabitEthernet, TenGigabitEthernet, TwentyFiveGigE, ...) is expanded, anything else passes through verbatim. ND removes an IOS-XE subinterface from the switch only under its canonical spelling, so the expansion is what keeps deleted / overridden working: TODO(4.2.1) xe-subinterface-remove-leaves-switch-interface (vault note written; same class as the ND leaves the IOS-XE port-channel interface on the switch after a port-channel is removed (4.2.1 and 4.3.1) #569 port-channel finding).
  • create_bulk uses the shared bulk_create_groups (one POST per switch and policy type); query_all manages the three policy types and tolerates the policy-less records a Catalyst switch list carries.
  • NX-OS branch: payload_defaults = {"mtu": 9216} because ND 4.3.1 refuses a subinterface create that omits mtu where 4.2.1 defaulted it (TODO(4.3.1) ethernet-create-required-fields-431, the nd_interface_{ethernet_trunk_host,ethernet_routed,ethernet_access (XE),vpc_trunk_host}: ND 4.3.1 rejects create/update bodies that omit fields 4.2.1 defaulted (allowedVlans, mtu) #564 class; vault note extended). Found by the 4.3.1 regression run of the existing NX-OS scenarios.
  • policy_type is visible in before/after output for both branches; argspec gains network_os_type and policy_type.
  • Docs: per-option applicability lines, the IOS-XE VRF guidance (omit vrf_interface for the global table; the Catalyst has no VRF named default), the controller-required ip / vlan_id on an iosXeSubinterface create, IOS-XE examples for merged and deleted.

Four questions (no new bulk stage or queue): the only bulk change is the create grouping; a mixed 207 is handled per item by the existing create_bulk / _request path exactly as in #571; the finalizer, except-path reader and remove-side keys are untouched.

Test Notes

  • Unit: tests/unit/ 4886 passed (24 new tests: enums, union dispatch, cross-branch rejection, XE ranges, parent-name expansion, mtu default, grouped bulk create keys, Catalyst list shapes).
  • Sanity: validate-modules, yamllint pass; ansible-lint passes on the new scenario; black / isort clean; pylint / mypy report only the pre-existing patterns the sibling orchestrators carry.
  • Lab, nd_interface_subinterface_managed full target (NX-OS scenarios on SITE1 S1_TOR1 / S3_TOR1 with Ethernet1/63 routedHost and Port-channel10 l3Po parents, plus the new xe.yaml on CAMPUS1 C1_LE1 / C3_LE1 Catalyst 9000v, parent GigabitEthernet1/0/2):
    • ND 4.2.1.10: ok=82 changed=22 failed=0 ignored=2 (the two ignored results are the cross-platform negative test)
    • ND 4.3.1.175: ok=82 changed=22 failed=0 ignored=2
    • xe.yaml converts the parent to an IOS-XE routed host and waits for discovery (ND checks the parent's DISCOVERED mode, not its intent), runs merged / replaced / overridden / deleted with both policy types, verifies the Catalyst running config is clean after deleted with abbreviated (lowercase) names, and resets the parent to the fabric-default trunk host.
  • Raw-REST probes on both controllers (intent-only and deployed) established: ip + vlanId are required on an iosXeSubinterface create; vrfInterface: default fails at deploy on the Catalyst; the only echoed default is adminState: true; mixed policy types in one interfaces[] are rejected; a lowercase remove orphans the switch interface (canonical name required).

Cisco Nexus Dashboard Version

4.2.1 (also verified on 4.3.1)

Related ND API Resource Category

  • analyze
  • infra
  • manage
  • onemanage
  • other

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_018LdsxVhpcrri7nbHXQxCcR

…nd iosXeSubinterfaceShutNoshut policy types

Extends nd_interface_subinterface_managed with the IOS-XE branch on the discriminated-union pattern of the ethernet (#558),
port-channel (#570) and SVI (#571) modules (issue #541):

- config_data.network_os is a union on network_os_type, injected as nx-os when omitted so existing playbooks are unchanged. The
  NX-OS SubinterfaceManagedPolicyModel moves onto InterfacePolicyStrictBase, so cross-platform fields fail before any controller call.
- The IOS-XE branch is a policy_type union of XeSubinterfacePolicyModel (iosXeSubinterface, injected when omitted; VLAN tag, L3
  addressing, VRF, with the ios_xe_int_subintf ranges: vlanId 1-4094, ipv6Prefix 64-127, description 1-200) and
  XeSubinterfaceShutNoshutPolicyModel (admin state only). Both templates are identical on ND 4.2.1 and 4.3.1; the ND-internal
  iosXeInternalSubinterface and userDefined are left out.
- interface_name accepts the Catalyst parent families: an abbreviated or lowercase prefix that matches exactly one canonical name
  (Ethernet, Port-channel, GigabitEthernet, TenGigabitEthernet, TwentyFiveGigE, ...) is expanded to it, anything else passes through
  verbatim instead of being rejected. ND removes an IOS-XE subinterface from the switch only under its canonical spelling
  (TODO(4.2.1) xe-subinterface-remove-leaves-switch-interface), so the expansion is what keeps state deleted / overridden working.
- create_bulk adopts bulk_create_groups (one POST per switch and policy type, #409): ND rejects an interfaces[] array that mixes the
  two IOS-XE policy types. query_all manages the three policy types and tolerates the policy-less records a Catalyst switch list
  carries.
- The NX-OS branch now always emits the int_subif template default mtu 9216 on the wire (payload_defaults): ND 4.3.1 refuses a
  create that omits it where 4.2.1 defaulted it (TODO(4.3.1) ethernet-create-required-fields-431, the #564 class).
- policy_type is visible in before/after output for both branches; the argspec gains network_os_type and policy_type.
- Module docs gain per-option applicability lines, the IOS-XE VRF guidance (omit vrf_interface for the global table; the Catalyst has
  no VRF named default) and the controller-required ip / vlan_id on an iosXeSubinterface create, plus IOS-XE examples.
- Unit tests for both branches, the union injection, cross-branch rejection, the XE ranges, the parent-name expansion, the mtu
  default, the grouped bulk create and the Catalyst switch-list shapes; xe.yaml integration scenario gated on nd_test_xe_switch_ip
  that converts the parent port to an IOS-XE routed host, waits for discovery, runs every state and resets the parent afterwards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LdsxVhpcrri7nbHXQxCcR
@allenrobel
allenrobel force-pushed the feat_541_subinterface_iosxe branch from f02099c to d0f3491 Compare September 18, 2026 18:40
deploys for all created subinterfaces for later bulk execution via `deploy_pending`.
Create multiple managed L3 subinterfaces in bulk. Groups subinterfaces by `(switch, policyType)` through the shared
`bulk_create_groups` (issue #409) and sends one POST per group with all of its subinterfaces in the `interfaces` array: ND
rejects an array that mixes policy types, which a Catalyst carrying both `iosXeSubinterface` and `iosXeSubinterfaceShutNoshut`

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.

Medium: Deploy successful subinterfaces from a mixed HTTP 207 response

Issue

When one bulk-create request contains both successful and failed subinterfaces, _request() raises as soon as it detects the failed item.

Deployment entries are queued only after _request() returns successfully. Therefore, subinterfaces ND accepted in the same response are never queued for deployment.

Evidence

  • subinterface_managed_interface.py:154-183 performs the bulk request before queueing deployments.
  • A mixed HTTP 207 causes _request() to raise before the queueing loop runs.
  • Existing tests do not cover success and failure inside the same response group.

Existing PR overlap

No matching comment or review thread exists on #572.

The same response-handling pattern was previously identified in #570 and #571, but #572 has its own orchestrator implementation and requires its own fix or a shared correction.

Existing open issue overlap

Impact

ND can store a successfully accepted subinterface while the switch never receives it. A retry may find matching ND intent and report no change, leaving ND and the switch inconsistent.

Practical example

A task creates two subinterfaces with deployment enabled:

cisco.nd.nd_interface_subinterface_managed:
  fabric_name: CAMPUS1
  config:
    - switch_ip: 10.122.84.182
      interface_name: GigabitEthernet1/0/1.100
      config_data:
        network_os:
          network_os_type: ios-xe
          policy:
            policy_type: iosXeSubinterface
            vlan_id: 100
            ip: 10.100.0.1
            prefix: 24

    - switch_ip: 10.122.84.182
      interface_name: GigabitEthernet1/0/1.200
      config_data:
        network_os:
          network_os_type: ios-xe
          policy:
            policy_type: iosXeSubinterface
            vlan_id: 200
            ip: 10.200.0.1
            prefix: 24

  config_actions:
    deploy: true
  state: merged

ND returns:

{
  "results": [
    {
      "name": "GigabitEthernet1/0/1.100",
      "status": "success"
    },
    {
      "name": "GigabitEthernet1/0/1.200",
      "status": "failed",
      "message": "VLAN is already in use"
    }
  ]
}

Current behavior:

.100 intent: accepted by ND
.200 intent: rejected
task: fails
.100 deployment: never queued
switch: .100 remains absent

Expected behavior:

.100 intent: accepted and queued for deployment
.200 intent: reported as failed
task: still fails honestly
failure finalizer: deploys accepted .100

Suggested fix

When _request() raises for a fresh HTTP 207 response:

  1. Inspect only that response per-item results.
  2. Correlate exact successful names with the requested models.
  3. Queue deployment for those accepted items.
  4. Re-raise the original failure.
  5. Allow the existing failure finalizer to deploy the accepted subset.

Add a test with one success and one failure inside the same (switch, policyType) group.

ND version applicability

The defect applies to the per-item HTTP 207 behavior used by both ND 4.2.1 and ND 4.3.1; it is not a version-specific schema difference.

- name: "XE DISCOVERY WAIT: Wait until ND has discovered .100, .101 and .102 on the Catalyst"
cisco.nd.nd_rest:
path: "/api/v1/manage/fabrics/{{ test_xe_fabric_name }}/switches/{{ xe_switch_id }}/interfaces"
method: get

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.

Medium: Reject subinterface removal until ND discovery is complete

Issue

A newly created IOS-XE subinterface may temporarily have a missing or unknown operData.operationalStatus.

The module currently allows removal during this discovery window. ND can delete its intent without generating the corresponding Catalyst-side removal, leaving the subinterface configured on the switch.

Evidence

  • subinterface_managed_interface.py:136-152 queues individual removals without checking discovery status.
  • subinterface_managed_interface.py:185-199 does the same for bulk removal.
  • This integration scenario polls for discovery before cleanup, but module users do not receive that protection.

Existing PR overlap

No matching comment or review thread exists on #572. Equivalent findings exist for the port-channel and SVI modules, but this PR has a separate subinterface removal implementation.

Existing open issue overlap

Impact

The Ansible task can report that the subinterface was removed while the physical switch still contains it.

Practical example

Create and deploy the subinterface:

cisco.nd.nd_interface_subinterface_managed:
  fabric_name: CAMPUS1
  config:
    - switch_ip: 10.122.84.182
      interface_name: GigabitEthernet1/0/1.100
      config_data:
        network_os:
          network_os_type: ios-xe
          policy:
            policy_type: iosXeSubinterface
            vlan_id: 100
            ip: 10.100.0.1
            prefix: 24
  config_actions:
    deploy: true
  state: merged

Immediately remove it:

cisco.nd.nd_interface_subinterface_managed:
  fabric_name: CAMPUS1
  config:
    - switch_ip: 10.122.84.182
      interface_name: GigabitEthernet1/0/1.100
  config_actions:
    deploy: true
  state: deleted

At that moment ND reports:

{
  "interfaceName": "GigabitEthernet1/0/1.100",
  "operData": {
    "operationalStatus": "unknown"
  }
}

Current result:

ND intent: removed
Catalyst configuration:
  interface GigabitEthernet1/0/1.100
  encapsulation dot1Q 100
  ip address 10.100.0.1 255.255.255.0

Expected result:

Cannot remove GigabitEthernet1/0/1.100 because ND has not finished
 discovering it (operationalStatus=unknown). Retry when the status is
 up or down. No changes were made.

Suggested fix

Reuse the raw interface inventory already fetched and cached by the orchestrator. Do not add another GET.

Before queueing any removal:

  • Allow operationalStatus: up.
  • Allow operationalStatus: down.
  • Reject missing, unknown, or unrecognized values.
  • Validate all requested removals before queueing any of them.
  • Apply the same validation in check mode.
  • Do not poll, sleep, or retry inside the module.

Users who want waiting behavior should eventually use a separate gathered-state task with Ansible until and retries.

ND version applicability

This discovery transition affects the IOS-XE lifecycle on both ND 4.2.1 and ND 4.3.1; it is not an OpenAPI schema difference.

- 802.1Q VLAN tag for the subinterface.
- Valid range is 2-4094.
- Valid range is 2-4094 for C(subinterface) and 1-4094 for C(iosXeSubinterface).
- Required by the controller when creating an C(iosXeSubinterface).

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.

Medium: Enforce IOS-XE create requirements before mutation

Issue

The module documentation says a new full iosXeSubinterface requires vlan_id and ip, but the model declares both fields as optional.

When the subinterface does not already exist, incomplete configuration passes model and check-mode validation and reaches the controller. This validation must be create-specific so sparse updates to an existing subinterface remain valid.

Evidence

  • The public option documentation here says vlan_id and ip are required by the controller on create.
  • subinterface_managed_interface.py:268-277 makes vlan_id, ip, and prefix optional.
  • The singular and bulk create paths send payloads without enforcing those requirements.
  • The shared create preflight verifies that a policy exists, but not whether the full IOS-XE policy has its create-required fields.

Existing PR overlap

No matching comment or review thread exists on #572.

Existing open issue overlap

Issue #541 owns the IOS-XE managed-subinterface acceptance criteria.

Impact

An incomplete playbook can pass local validation and check mode but fail only after an actual controller request.

Practical example

Assume this subinterface does not exist:

cisco.nd.nd_interface_subinterface_managed:
  fabric_name: CAMPUS1
  config:
    - switch_ip: 10.122.84.182
      interface_name: GigabitEthernet1/0/1.100
      config_data:
        network_os:
          network_os_type: ios-xe
          policy:
            policy_type: iosXeSubinterface
            admin_state: true
  config_actions:
    deploy: false
  state: merged

Current local payload:

{
  "interfaceName": "GigabitEthernet1/0/1.100",
  "configData": {
    "networkOS": {
      "networkOSType": "ios-xe",
      "policy": {
        "policyType": "iosXeSubinterface",
        "adminState": true
      }
    }
  }
}

Missing create data:

vlanId
ip
prefix

Current behavior:

model validation: passes
check mode: can report the create as viable
normal mode: sends the incomplete request to ND
controller: rejects the create

Expected behavior:

Cannot create IOS-XE subinterface GigabitEthernet1/0/1.100:
vlan_id, ip, and prefix are required for a new iosXeSubinterface.
No changes were made.

This existing-interface update must remain valid:

policy:
  policy_type: iosXeSubinterface
  admin_state: false

The separate admin-only template must also remain valid without addressing fields:

policy:
  policy_type: iosXeSubinterfaceShutNoshut
  admin_state: false

Suggested fix

Use the state machine already-computed create subset and existing inventory snapshot; do not add another GET.

For an absent interface using the full iosXeSubinterface policy:

  • Require vlan_id.
  • Require ip.
  • Require its paired prefix.
  • Aggregate all incomplete create items into one error.
  • Apply the same validation in normal and check mode.

Do not apply the requirement to sparse updates of an existing subinterface, state: deleted, or the iosXeSubinterfaceShutNoshut admin-only policy.

Add tests covering incomplete creates, valid full creates, sparse updates, admin-only policies, bulk input, and check-mode parity.

ND version applicability

Neither ND 4.2.1 nor ND 4.3.1 marks these fields as required in the published schema. This requirement comes from the public module documentation and verified controller create behavior, so it should be treated as a live cross-version create requirement rather than an OpenAPI requirement.

vrf_interface: str | None = Field(
default=None, alias="vrfInterface", min_length=1, max_length=32, description="Interface VRF name; use `default` for default VRF"
)
ip: str | None = Field(default=None, alias="ip", description="IPv4 address of the subinterface")

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.

Medium: Validate IOS-XE IPv4 and IPv6 address syntax locally

Issue

The IOS-XE managed-subinterface model treats ip and ipv6 as unrestricted strings.

Its pair validators only check that an address and prefix appear together. They do not check whether the address is actually valid.

Evidence

  • subinterface_managed_interface.py:274-277 defines both address fields as strings.
  • Supplying a numeric prefix satisfies the pair validator even when the address is malformed.
  • Both ND 4.2.1 and ND 4.3.1 schemas identify these fields as IPv4 and IPv6 values.

Existing PR overlap

No matching comment or review thread exists on #572.

Existing open issue overlap

Issue #541 owns strict validation for the new IOS-XE managed-subinterface policy.

Impact

Invalid addresses pass model validation and check mode. Normal execution discovers the error only after making a controller request.

Practical example

This configuration includes every create-required field, but both addresses are invalid:

cisco.nd.nd_interface_subinterface_managed:
  fabric_name: CAMPUS1
  config:
    - switch_ip: 10.122.84.182
      interface_name: GigabitEthernet1/0/1.100
      config_data:
        network_os:
          network_os_type: ios-xe
          policy:
            policy_type: iosXeSubinterface
            vlan_id: 100
            ip: 999.999.999.999
            prefix: 24
            ipv6: not-an-ipv6
            ipv6_prefix: 64
  config_actions:
    deploy: false
  state: merged

Current behavior:

ip and prefix are both present: passes pair validation
ipv6 and ipv6_prefix are both present: passes pair validation
check mode: can report the configuration as viable
normal mode: sends malformed addresses to ND
controller: rejects the request

Expected behavior:

Invalid IPv4 address for ip: 999.999.999.999
Invalid IPv6 address for ipv6: not-an-ipv6
No changes were made.

A correct configuration should continue to pass:

policy:
  policy_type: iosXeSubinterface
  vlan_id: 100
  ip: 10.100.0.1
  prefix: 24
  ipv6: 2001:db8:100::1
  ipv6_prefix: 64

Suggested fix

Use the collection shared strict address types for proposed configuration:

  • IPv4Host or the appropriate strict IPv4 host type for ip.
  • IPv6Host or the appropriate strict IPv6 host type for ipv6.

Retain the existing address/prefix pair validators.

Add tests proving that malformed IPv4 and IPv6 values fail locally, valid addresses serialize correctly, normal and check mode return the same validation error, controller-response parsing remains tolerant of legitimate echoed values, and no controller GET or mutation is added for this validation.

ND version applicability

Both ND 4.2.1 and ND 4.3.1 specify the corresponding IPv4 and IPv6 formats, so this finding applies equally to both supported versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2.0.0 Release 2.0.0 ready for review Submitter is requesting a PR review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants