Skip to content

nd_interface_ethernet_trunk_host#265

Open
allenrobel wants to merge 20 commits into
nd_interface_ethernet_accessfrom
nd_interface_ethernet_trunk_host
Open

nd_interface_ethernet_trunk_host#265
allenrobel wants to merge 20 commits into
nd_interface_ethernet_accessfrom
nd_interface_ethernet_trunk_host

Conversation

@allenrobel
Copy link
Copy Markdown
Collaborator

@allenrobel allenrobel commented Apr 22, 2026

Related Issue(s)

Stacks on top of #222 (access module). Replaces #264, which was auto-closed when its source branch was renamed.

Proposed Changes

Adds the nd_interface_ethernet_trunk_host module for managing Cisco Nexus Dashboard 4.2 ethernet interfaces with the trunkHost policy type. Parallels nd_interface_ethernet_access: composite (switch_ip, interface_name) identifier, bulk create / bulk normalize-as-delete, per-interface deploy queueing, and the full merged / replaced / overridden / deleted state lifecycle.

  • New EthernetTrunkHostInterfaceModel with trunk-specific fields — allowed_vlans (regex-validated, accepts none, all, or comma-separated IDs/ranges), native_vlan, vlan_mapping, and nested vlan_mapping_entries with customer_vlan_id as List[str] to match the ND API.
  • TrunkHostPolicyTypeEnum added to shared interface enums.
  • EthernetTrunkHostInterfaceOrchestrator filters query_all to exclude interfaces matching the unconfigured int_trunk_host default signature, so state: overridden is idempotent on the second run. Normalizing a trunkHost interface produces another trunkHost interface, so without this filter every fabric-default interface would appear in before and be re-queued for normalization on every run.
  • InterfaceDefaultPolicyModel gains description: "" and nativeVlan: 1 so interfaceActions/normalize actually clears user-set descriptions and native VLANs on deleted / overridden interfaces instead of leaving them behind. This affects all ethernet modules that delete via normalize, which is the desired behavior.
  • allowed_vlans validator coerces bare-integer ND responses (e.g. a single VLAN returned as 999) to string before regex validation.
  • File-based logging integrated via setup_logging() in main(). Module-level nd.nd_interface_ethernet_trunk_host logger emits debug records at expand_config, manage_state begin/end, and on NDStateMachineError (via log.exception so tracebacks reach the log file regardless of output_level). Logging activates only when ND_LOGGING_CONFIG points at a logging.config.dictConfig JSON file; otherwise the calls are no-ops.
  • Integration tests cover all four states. selectattr('interface_name', ...) lookups are scoped by switch_ip so assertions hold on multi-switch fabrics. The VLAN-mapping test block is gated on a supports_vlan_mapping variable because Nexus 9000v virtual switches reject switchport vlan mapping ... dot1q-tunnel ... at the NX-OS layer. The test target's tasks/main.yaml wraps its include_tasks in a block that forwards nd_logging_config (set in inventory.networking [nd:vars]) into the module subprocess as ND_LOGGING_CONFIG, since ansible-test strips the controller's shell environment.

Test Notes

  • Full integration suite passes against an ND 4.2 testbed with two Nexus 9000v switches in the target fabric: ansible-test network-integration nd_interface_ethernet_trunk_host -v.
  • Merged, replaced, overridden (including idempotent re-run), and deleted states all pass.
  • VLAN-mapping block is skipped by default; to exercise it on a hardware testbed add supports_vlan_mapping=true to the [nd:vars] section of tests/integration/inventory.networking. (ansible-test does not accept --extra-vars on the command line.)
  • To enable file-based logging during an integration run, add nd_logging_config=/absolute/path/to/logging_config.json to [nd:vars] in tests/integration/inventory.networking. An example config conforming to logging.config.dictConfig is documented in plugins/module_utils/common/log.py.
  • Unit tests cover the two branch-new source files with 142 tests and ~99% line coverage:
    • tests/unit/module_utils/models/test_ethernet_trunk_host_interface.py exercises every nested Pydantic model (VlanMappingEntry, Policy, NetworkOS, ConfigData, Interface) with field validation, range and enum constraints, validators, serializers, round-trip to_payload / to_config / from_response / from_config, composite identifier, diff, merge, and argument_spec shape.
    • tests/unit/module_utils/orchestrators/test_ethernet_trunk_host_interface.py wires a real RestSend with the file-based Sender from tests/unit/module_utils/sender_file.py and ResponseHandler to verify _managed_policy_types, the _is_unconfigured_default truth table, and query_all filtering (policy-type + unconfigured-default) across multiple switches.
    • Parametrize collapses all value-table tests (range checks, enum choice checks, truth table, input/output tables) into single functions with descriptive ids= labels so pytest output still names each case.
    • Run with: python -m pytest tests/unit/module_utils/models/test_ethernet_trunk_host_interface.py tests/unit/module_utils/orchestrators/test_ethernet_trunk_host_interface.py

Cisco Nexus Dashboard Version

4.2

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 Apr 22, 2026
@allenrobel allenrobel marked this pull request as draft April 22, 2026 04:58
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 9620a8f to 74f70ae Compare April 22, 2026 20:28
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 03b45b0 to 94e8389 Compare April 22, 2026 20:29
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 74f70ae to c05c9b4 Compare April 23, 2026 01:40
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 94e8389 to c80fa2f Compare April 23, 2026 01:40
@allenrobel allenrobel changed the title Add nd_interface_ethernet_trunk_host module nd_interface_ethernet_trunk_host module Apr 23, 2026
@allenrobel allenrobel marked this pull request as ready for review April 23, 2026 02:02
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch 2 times, most recently from 7f93144 to b556f6d Compare April 24, 2026 01:48
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from a6e4008 to 64cebc1 Compare April 24, 2026 01:54
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from b556f6d to e0d0b54 Compare April 24, 2026 01:54
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 0d3c857 to 0bc5c99 Compare April 24, 2026 20:34
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 8989c2c to 63772b4 Compare April 24, 2026 20:34
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 0bc5c99 to 6f98652 Compare April 28, 2026 01:37
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 63772b4 to 4ef3a29 Compare April 28, 2026 01:43
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 6f98652 to bcff933 Compare April 28, 2026 20:25
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch 3 times, most recently from bba7fdc to a92738c Compare April 29, 2026 18:47
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from eca8c3d to a02ccf5 Compare April 29, 2026 18:47
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 4058f65 to be6663b Compare May 9, 2026 00:36
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 724ba60 to 567f385 Compare May 9, 2026 00:36
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from be6663b to 80f9619 Compare May 11, 2026 20:31
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 567f385 to 61e8706 Compare May 11, 2026 20:31
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 80f9619 to 16e8ffa Compare May 14, 2026 18:39
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 61e8706 to b115f74 Compare May 14, 2026 18:39
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from 16e8ffa to ba57b6c Compare May 14, 2026 20:44
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from b115f74 to 3961059 Compare May 14, 2026 20:44
@allenrobel allenrobel force-pushed the nd_interface_ethernet_access branch from ba57b6c to c388ad2 Compare May 20, 2026 21:23
@allenrobel allenrobel force-pushed the nd_interface_ethernet_trunk_host branch from 3961059 to 915bdc3 Compare May 20, 2026 21:23
allenrobel and others added 20 commits May 29, 2026 12:47
Adds a new Ansible module for CRUD operations on host-facing ethernet
trunk interfaces in Nexus Dashboard 4.2. Follows the same composite-
identifier, bulk-CRUD, and deploy-lifecycle pattern as the access
module, filtering on policyType=trunkHost.

- TrunkHostPolicyTypeEnum added to shared interface enums.
- EthernetTrunkHostInterfaceModel defines trunk-specific policy fields
  (allowed_vlans with regex validator, native_vlan, vlan_mapping,
  vlan_mapping_entries) and reuses shared enums for the rest.
- EthernetTrunkHostInterfaceOrchestrator inherits the shared ethernet
  base; only _managed_policy_types() is type-specific.
- Integration test target covers merged/replaced/overridden/deleted.
  Unit tests deferred to match nd_interface_ethernet_access.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolves issues surfaced by the full trunk-host integration run:

- `EthernetTrunkHostVlanMappingEntryModel.customer_vlan_id` is now
  `List[str]`, matching the ND API which expects `customerVlanId` as an
  array of VLAN tokens rather than a single string.
- `EthernetTrunkHostPolicyModel.validate_allowed_vlans` now coerces
  integer input to string before regex validation, so ND responses that
  return a single-VLAN `allowedVlans` as a bare integer (e.g. `999`) no
  longer raise a Pydantic `ValueError`.
- `InterfaceDefaultPolicyModel` now includes `description: ""` and
  `nativeVlan: 1`, so `interfaceActions/normalize` fully resets user-set
  descriptions and native VLANs on deleted / overridden interfaces
  instead of leaving them behind.
- `EthernetTrunkHostInterfaceOrchestrator.query_all` filters out
  interfaces matching the unconfigured `int_trunk_host` default
  signature. Normalizing a trunkHost interface produces another
  trunkHost interface, so without this filter `state: overridden` could
  not be idempotent (every fabric-default interface appeared in `before`
  and was re-queued for normalization on every run).
- Integration tests scope `selectattr('interface_name', ...)` lookups
  by `switch_ip` so assertions work on multi-switch fabrics, and the
  VLAN-mapping block is gated on a new `supports_vlan_mapping` extra-var
  because Nexus 9000v virtual switches reject
  `switchport vlan mapping ... dot1q-tunnel ...`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Migrate to builtin generics and union syntax in files introduced on this
branch: Optional[X] -> X | None, List[X] -> list[X], Dict -> dict,
Set -> set, Type -> type. Drop the now-unused imports from typing.

Add `from __future__ import annotations` to the model file so sanity
pylint tolerates PEP 604 union syntax under older py-version settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cover the two branch-new source files with 142 tests and ~99% line
coverage:

- `tests/unit/module_utils/models/test_ethernet_trunk_host_interface.py`
  exercises every nested Pydantic model (VlanMappingEntry, Policy,
  NetworkOS, ConfigData, Interface) with field validation, range and
  enum constraints, validators, serializers, round-trip to_payload /
  to_config / from_response / from_config, composite identifier, diff,
  merge, and argument_spec shape.

- `tests/unit/module_utils/orchestrators/test_ethernet_trunk_host_interface.py`
  wires a real `RestSend` with the file-based `Sender` from
  `tests/unit/module_utils/sender_file.py` and `ResponseHandler` to
  verify `_managed_policy_types`, the `_is_unconfigured_default` truth
  table, and `query_all` filtering (policy-type + unconfigured-default)
  across multiple switches. Fixture JSON added under
  `tests/unit/module_utils/fixtures/fixture_data/`.

Parametrize collapses all value-table tests (range checks, enum choice
checks, truth table, input/output tables) into single functions with
descriptive `ids=` labels so pytest output still names each case.

Adds an empty `tests/unit/module_utils/orchestrators/__init__.py` to
satisfy ansible-test's `empty-init` sanity check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire setup_logging() into the module's main() and emit debug records at
expand_config, manage_state begin/end, and on NDStateMachineError.
Logging activates only when ND_LOGGING_CONFIG points at a logging.config
JSON file; otherwise the calls are no-ops.

ansible-test strips the controller's shell environment, so the
integration test's main.yaml now wraps its include_tasks in a block that
forwards nd_logging_config (set in inventory.networking [nd:vars]) into
the module subprocess as ND_LOGGING_CONFIG. Updated the header comment
to document the variable and correct the supports_vlan_mapping example
to use inventory.networking instead of the unsupported --extra-vars
flag.
Replaces the inline `description: str | None` with the shared
`AsciiDescription` Annotated type. Same `max_length=254` constraint via
Field(...).

Adds parametrized tests for em-dash, smart quotes, emoji, and latin-1
rejection alongside ASCII passthrough.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`FabricContext.validate_for_mutation` now also fetches
`/deploymentFreeze` after the fabric summary, so the orchestrator's
`query_all` consumes one extra response from the queue. Insert a
`deploymentFreeze: false` fixture between the summary and switch list
yields in both the happy-path (00400) and all-default (00410) tests
to keep the response generator aligned.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The OpenAPI intTrunkHostTemplate declares stormControlBroadcastLevel,
stormControlMulticastLevel, and stormControlUnicastLevel as
number/float, but the model and argument spec had them as str. Switch
to float | None with ge=0.0/le=100.0 so the model matches the API
contract and the argument spec validates percentage input correctly.
Update DOCUMENTATION block to type: float to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-policy interface modules (one module = one policyType) must not
expose policy_type as a user-facing argspec option. The
nd_interface_ethernet_trunk_host module targets trunkHost only, so
policy_type was dead surface area.

- Remove policy_type from argspec and DOCUMENTATION
- Drop normalize_policy_type validator and serialize_policy_type
  field serializer
- Hardcode TrunkHostPolicyTypeEnum.TRUNK_HOST as the model default;
  field still serialized into payloads as policyType
- Strip policy_type from integration test inputs
- Update unit tests (122/122 pass)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace shape-only regex with Annotated types (AllowedVlans,
CustomerVlanIdList) backed by a shared per-token validator that
enforces VLAN ids in 1..4094 and rejects reversed ranges. Mirrors the
fix applied to nd_interface_port_channel_trunk_host. Mark both types
with a TODO to consolidate into models/types.py once sibling branches
merge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
VlanId (1..4094) recurs as Field(ge=1, le=4094) on multiple fields
across the interface family. Defer the cleanup to the same post-merge
PR that introduces AllowedVlans / CustomerVlanIdList in models/types.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace verbatim-repeated module argument blocks (check / normal / idempotent
runs of the same call) with YAML anchors. Removes a class of typo where one
of the duplicated blocks could drift from its siblings.

Anchors are defined at first use within each section to keep them local to
the reader.
Move the top-of-file SETUP block from merged.yaml into a dedicated
tasks/setup.yaml and call it from main.yaml before the state-test blocks.
Includes the post-cleanup DEBUG re-query probe alongside the cleanup since
both are file-level observability rather than test-specific.

Intra-test setup that's coupled to specific tests stays inline (the
Ethernet1/48 cleanups after the vlan_mapping and no-deploy tests in
merged.yaml). The final CLEANUP at the bottom of deleted.yaml stays inline
as well — it's post-test teardown for the last state file, not file-level
pre-test prep.
…l tests

validate_for_mutation reads freeze status from cached fabric_summary
(see fabric_context.py:166), so query_all no longer issues a separate
GET to /deploymentFreeze. Applies to both the happy-path 00400 test
(which was failing) and the all-defaults 00410 test (which passed by
luck because it expects an empty result).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…spec

Same change as ethernet_access (5ecd435): drop interface_type, mode,
and network_os_type from get_argument_spec() and DOCUMENTATION; narrow
each in the Pydantic model to Literal[<value>] (or keep the existing
Enum for policy_type) and add Field(frozen=True) so the values are
immutable post-construction. Updates the argspec assertion test and
the network_os_type test to verify the Literal lock instead of
accepting "ios-xe".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EthernetBaseOrchestrator.query_all now only iterates switches whose
switch_ip appears in rest_send.params["config"] (unless state is
overridden). The trunk_host orchestrator tests still built RestSend
with only check_mode/fabric_name, so _switches_to_query() returned {}
and test_00400 hit assert len(result) == 2 with 0.

_build_rest_send / _build_orchestrator now accept a params dict that
is merged into the RestSend params, mirroring the access-orchestrator
test helper. test_00400 supplies state=merged plus both switch IPs.

test_00410 had been passing for the wrong reason — the switch was
never queried, so the empty result said nothing about the filter.
It now supplies state=merged plus the fixture switch_ip so the per-
switch GET fires, and spies on _is_unconfigured_default to assert the
filter was actually invoked on both interfaces returned by the switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carry over nine fixes that landed on the ethernet_access parent but were
not yet applied to the cloned trunk_host module. Each gap was identified
in a side-by-side audit of the two branches.

Module (plugins/modules/nd_interface_ethernet_trunk_host.py):

- Add validate_interface_names, validate_within_item_duplicates, and
  validate_across_item_duplicates so null/empty entries and
  case-insensitive duplicates raise friendly ValueErrors instead of
  crashing downstream on name.lower() or silently expanding into
  identical orchestrator items.
- Make expand_config null-safe (`group.get("interface_names") or []`)
  so an explicit YAML `interface_names: ~` no longer crashes with
  TypeError.
- Update expand_config docstring to list the ValueError conditions.
- Wrap the expand_config call site in try/except ValueError so
  configuration errors surface via module.fail_json instead of an
  unhandled traceback.
- Add a broad `except Exception` handler in main() to format
  unexpected exceptions consistently with the NDStateMachineError path.

Model (plugins/module_utils/models/interfaces/ethernet_trunk_host_interface.py):

- Rewrite normalize_interface_name to Title-case the full leading
  alphabetic prefix via _INTERFACE_NAME_PREFIX_RE so mixed-case input
  (etHernet1/1, ETHERNET1/1) canonicalizes to Ethernet1/1 instead of
  EtHernet1/1, restoring idempotency on subsequent reads.
- Default EthernetTrunkHostConfigDataModel.network_os via
  default_factory=EthernetTrunkHostNetworkOSModel so an empty
  config_data builds without a `field required` ValidationError.
- Add a wrap-mode model_serializer that strips the hardcoded
  policy_type ("trunkHost") from to_config() output while leaving
  to_payload() and to_diff_dict() untouched, keeping playbook
  before/after diffs free of frozen scaffolding fields.

Tests (tests/unit/module_utils/models/test_ethernet_trunk_host_interface.py):

- Drop policy_type from SAMPLE_ANSIBLE_CONFIG so round-trip
  assertions match the new to_config output.
- Invert test 00470 to verify the network_os default_factory
  behaviour instead of the prior `field required` expectation.
- Update test 00710 to assert policy_type is omitted from to_config
  output and add 00711 to assert to_payload still emits the
  wire-form policyType.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The base orchestrator's new selective delete dispatcher routes
ethernet interfaces with `bandwidth`, `debounceLinkupTimer`, or
`inheritBandwidth` set to a per-interface PUT-as-replace reset path.
For that path to ever fire on a trunkHost interface, the existing
`EthernetTrunkHostInterfaceOrchestrator._is_unconfigured_default`
filter has to recognise that an interface with one of those Class C
fields set is NOT unconfigured — otherwise it falls out of `existing`
and `_manage_delete_state` never queues it for delete.

Extends the predicate to additionally reject any interface whose
policy carries a non-null value for any field in
`InterfaceDefaultConfig.UNRESETTABLE_FIELDS`. Parametrized truth-table
test 00200 gains three new ids (`class_c_bandwidth`,
`class_c_debounce_linkup`, `class_c_inherit_bandwidth`) pinning the
extended behaviour.

Integration scenario in
`tests/integration/targets/nd_interface_ethernet_trunk_host/tasks/deleted.yaml`
seeds bandwidth=1500000 on Ethernet1/48 via `state: merged`, runs
`state: deleted`, and asserts the field clears. Lab-verified
end-to-end on ND 4.2.1 (SITE1 / S1_LE3 Ethernet1/41 — see the playbook
in the audit work).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant