Skip to content

nd_fabric_update_group - ND 4.2 software update groups#285

Open
allenrobel wants to merge 5 commits into
developfrom
nd_fabric_update_group
Open

nd_fabric_update_group - ND 4.2 software update groups#285
allenrobel wants to merge 5 commits into
developfrom
nd_fabric_update_group

Conversation

@allenrobel
Copy link
Copy Markdown
Collaborator

@allenrobel allenrobel commented May 20, 2026

Note

Draft — stacked on nd_interface_loopback (the PR base branch).

Related Issue(s)

Proposed Changes

  • New module nd_fabric_update_group — manages Fabric Software Management update groups (the ND 4.2 successor to ND 3.x image policies) within a fabric.
  • Gen 3 architecture: Pydantic FabricUpdateGroupModel, FabricUpdateGroupOrchestrator, smart endpoints, and NDStateMachine wiring.
  • States supported: merged, replaced, overridden, deleted.
  • The write path uses the switch-centric softwareUpdatePlan/actions API, which is ghost-safe by construction:
    • attachGroup creates a group and assigns its switches (requires at least one switch).
    • detachGroup removes switches; ND auto-deletes a group once its last switch is removed.
    • Group settings are applied via PUT /updateGroups/{name}, built by overlaying the user's fields onto a GET of the current group, so the PUT echoes the live membership and never moves switches.
  • New force_created per-group option — ND raises a pre-flight warning when a group's switch set would cover all switches of a role; force_created: true acknowledges the warning so the change is applied. A warning otherwise fails the task, since ND applies nothing when it warns.
  • Reads (query_one / query_all, used by overridden reconciliation) stay on the group-centric GET endpoints.
  • New endpoint module endpoints/v1/manage/software_update_plan_actions.py (attachGroup / detachGroup); the now-unused group-centric POST endpoint is removed.
  • update_group_switches / installation_order_devices accept either switch IP addresses or serial numbers (switchIds); IPs are resolved to switchIds via FabricContext.
  • ND 4.2.1 wire workaround: installationOrderDevices is silently dropped by ND on write — excluded from the idempotency diff (TODO(4.2.1) marker). ND's system-managed default group (literally named "None") is filtered out of query results so overridden does not try to delete it.

Background — the "ghost group" defect this design sidesteps

The group-centric updateGroups CRUD endpoints have no minimum-switch guard and cannot represent a zero-switch group: reusing a switch between groups silently drained a group and left it an unreadable "ghost" (omitted from the list, HTTP 400 on the single-group GET, name still reserved). The switch-centric action API used here is ghost-safe by construction — attachGroup requires at least one switch and detachGroup cleans up emptied groups — so the module no longer depends on an ND-side fix. This module was previously postponed on that defect; the redesign removes the block.

Test Notes

  • Unit tests: 80 for this module (model + orchestrator + endpoints). Full suite 552 passing, no regressions.
  • black / isort / mypy clean; ansible-test sanity --docker passes.
  • Live verification on ND 4.2.1 (fabric SITE1) of behaviors the OpenAPI spec cannot confirm: detachGroup last-switch auto-delete, PUT with current membership is a membership no-op, and attachGroup forceCreated semantics (a warning status means ND applied nothing).
  • Integration target tests/integration/targets/nd_fabric_update_group/merged, replaced, overridden, deleted all pass against live ND 4.2.1.

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 changed the title Add nd_fabric_update_group module (ND 4.2 update groups) nd_fabric_update_group May 20, 2026
@allenrobel allenrobel self-assigned this May 20, 2026
@allenrobel allenrobel marked this pull request as ready for review May 20, 2026 20:24
@allenrobel allenrobel force-pushed the nd_interface_loopback branch from bff5d88 to 921294d Compare May 20, 2026 21:23
@allenrobel allenrobel force-pushed the nd_fabric_update_group branch from d537edc to 25ce389 Compare May 20, 2026 21:23
Base automatically changed from nd_interface_loopback to develop May 21, 2026 13:26
@allenrobel allenrobel force-pushed the nd_fabric_update_group branch from 3c80477 to 67167cd Compare May 21, 2026 19:41
@allenrobel allenrobel changed the title nd_fabric_update_group nd_fabric_update_group - ND 4.2 software update groups May 27, 2026
allenrobel and others added 4 commits May 28, 2026 07:33
Add the nd_fabric_update_group module to manage Fabric Software Management
update groups (the ND 4.2 successor to NDFC/ND 3.x image policies). Built on
the Gen 3 architecture: FabricUpdateGroupModel, FabricUpdateGroupOrchestrator,
smart endpoints, and NDStateMachine. Supports merged, replaced, overridden,
and deleted states.

- update_group_switches / installation_order_devices accept switch IPs or
  serial numbers; IPs are resolved to switchIds via FabricContext.
- Two ND 4.2.1 wire workarounds: installationOrderDevices is silently dropped
  on write (excluded from the idempotency diff, TODO(4.2.1)); the ND-managed
  default group named "None" is filtered from query results.
- Adds UpdateGroupNameMixin to endpoints/mixins.py.
- 58 unit tests (model + orchestrator) and an integration test target.

Note: this work is postponed pending an ND-side fix for a zero-switch
"ghost group" defect that breaks idempotent reconciliation of overridden
state. A bug has been filed with the ND developers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The group-centric updateGroups CRUD endpoints have no minimum-switch
guard and cannot represent a zero-switch group, so reusing a switch
between groups silently left an unreadable "ghost" group behind. Rebuild
the write path on the ghost-safe softwareUpdatePlan/actions API:
attachGroup creates groups and assigns switches, detachGroup removes
switches (ND auto-deletes an emptied group), and group settings are
applied via PUT built from a GET of the current group so it never moves
membership. Add a force_created option so a group whose membership trips
an ND pre-flight warning can be applied. Reads stay on the group-centric
GET endpoints; the now-unused group-centric POST endpoint is removed.

Verified live on ND 4.2.1: detachGroup last-switch auto-delete, PUT
membership no-op, and attachGroup forceCreated semantics (a warning
status means ND applied nothing, so it always fails the task).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the FabricUpdateGroupModel annotations from typing.Optional /
List / Dict to the PEP 604 / PEP 585 forms (X | None, list[X], dict) per
the collection's type-annotation standard, and trim the now-unused
typing imports. Annotation-only change; verified clean against
ansible-test sanity (compile / import / pep8) on Python 3.8-3.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose ND's fabric-wide softwareUpdatePlan "propose" action as a new
top-level auto_assign option (roleBased / evenOdd), letting ND
auto-generate update groups instead of defining them explicitly in
config.

The auto-assign path bypasses NDStateMachine (its per-group config-diff
state model does not fit a single fabric-level action) and derives
changed from a before/after query_all snapshot. auto_assign is mutually
exclusive with config and valid only with state merged or overridden;
check mode reports no change since the propose action cannot be
previewed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 28, 2026 17:34
@allenrobel allenrobel force-pushed the nd_fabric_update_group branch from 67167cd to fbf128d Compare May 28, 2026 17:34
Copy link
Copy Markdown

Copilot AI left a comment

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 adds the new nd_fabric_update_group Ansible module for managing Cisco Nexus Dashboard 4.2 Fabric Software Management update groups, including group lifecycle, switch-centric attach/detach actions, auto-assignment, and tests.

Changes:

  • Adds the module entry point, Pydantic model, orchestrator, endpoint definitions, and endpoint mixin for update groups.
  • Implements switch-centric write behavior via softwareUpdatePlan/actions while retaining group-centric reads/settings updates.
  • Adds unit and integration coverage for models, endpoints, orchestrator behavior, state handling, and auto-assign workflows.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plugins/modules/nd_fabric_update_group.py Adds the Ansible module and auto-assign execution path.
plugins/module_utils/orchestrators/fabric_update_group.py Implements update group CRUD orchestration and switch ID/IP normalization.
plugins/module_utils/models/fabric_update_group/fabric_update_group.py Adds the update group Pydantic model and module argument spec.
plugins/module_utils/models/fabric_update_group/__init__.py Adds model package initialization.
plugins/module_utils/endpoints/v1/manage/fabric_update_group.py Adds group-centric GET/PUT/DELETE endpoint classes.
plugins/module_utils/endpoints/v1/manage/software_update_plan_actions.py Adds attach/detach/propose action endpoint classes.
plugins/module_utils/endpoints/mixins.py Adds UpdateGroupNameMixin.
tests/unit/module_utils/models/test_fabric_update_group.py Adds model/unit argument-spec tests.
tests/unit/module_utils/orchestrators/test_fabric_update_group.py Adds orchestrator unit tests.
tests/unit/module_utils/endpoints/test_software_update_plan_actions.py Adds action endpoint tests.
tests/unit/module_utils/fixtures/fixture_data/test_fabric_update_group.json Adds orchestrator response fixtures.
tests/integration/targets/nd_fabric_update_group/tasks/main.yaml Adds integration test entrypoint.
tests/integration/targets/nd_fabric_update_group/tasks/setup.yaml Adds integration cleanup setup.
tests/integration/targets/nd_fabric_update_group/tasks/merged.yaml Adds merged-state integration tests.
tests/integration/targets/nd_fabric_update_group/tasks/replaced.yaml Adds replaced-state integration tests.
tests/integration/targets/nd_fabric_update_group/tasks/overridden.yaml Adds overridden-state integration tests.
tests/integration/targets/nd_fabric_update_group/tasks/deleted.yaml Adds deleted-state integration tests.
tests/integration/targets/nd_fabric_update_group/tasks/auto_assign.yaml Adds auto-assign integration tests.
tests/integration/targets/nd_fabric_update_group/vars/main.yaml Adds integration variables and sample group configs.

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

Comment on lines +446 to +470
Update a fabric update group: reconcile membership against the current wire state
(`attachGroup` for added switches, `detachGroup` for removed switches), then apply settings
via PUT.

## Raises

### RuntimeError

- If `update_group_switches` resolves to an empty set (an empty update group is not permitted -
use `state: deleted`), a switch IP cannot be resolved, a request fails, or an action
endpoint reports a non-success status.
"""
try:
update_group_name = model_instance.update_group_name
current_raw = self._get_group_raw(update_group_name)
current_ids = list(current_raw.get("updateGroupSwitches") or [])
desired_ids = [self._resolve_switch_id(s) for s in (model_instance.update_group_switches or [])]
if not desired_ids:
raise RuntimeError("update_group_switches must be non-empty; an empty update group is not permitted (use state: deleted)")
to_add = [s for s in desired_ids if s not in current_ids]
to_remove = [s for s in current_ids if s not in desired_ids]
if to_add:
self._attach([self._attach_item(model_instance, switch_ids=to_add)])
if to_remove:
self._detach([{"updateGroupName": update_group_name, "switchIds": to_remove}])
Comment on lines +349 to +352
try:
return self._get_group_raw(update_group_name)
except Exception: # pylint: disable=broad-except
return None
Comment on lines +69 to +72
segments = ["fabrics", self.fabric_name, "updateGroups"]
if self.update_group_name is not None:
segments.append(self.update_group_name)
return BasePath.path(*segments)
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.

2 participants