ND Manage VRFs & Networks: Add Internal Staged Workflow - #509
Conversation
661a7f6 to
47d5826
Compare
| - V(deleted) removes specified VRFs (or all if config is empty). | ||
| - V(gathered) returns current VRF state (the only state allowed on child | ||
| fabrics when targeted directly). | ||
| - V(staged) is an internal/private workflow state. It follows |
There was a problem hiding this comment.
Why is staged called “internal/private” while also being included in the module’s public choices?
There was a problem hiding this comment.
Since "state" comes under the argspec, the choices for the state needs documentation else get's flagged by validate-module sanity.
There was a problem hiding this comment.
did you consider exposing this "feature" through an undocumented environment variable that can be specified in at task level? this way the documentation would remain clean
There was a problem hiding this comment.
@AKDRG just remove the usage of internal/private as last conversation on this was to fully document it as a state.
There was a problem hiding this comment.
My bad, was thinking we needed to document it as an internal state. Amended the wordings now.
| - V(deleted) removes specified VRFs (or all if config is empty). | ||
| - V(gathered) returns current VRF state (the only state allowed on child | ||
| fabrics when targeted directly). | ||
| - V(staged) is an internal/private workflow state. It follows |
There was a problem hiding this comment.
Should we add example for this state with description what it is actually doing?
There was a problem hiding this comment.
Since it's an private state and not intended for customer use cases, I guess we need not document examples.
There was a problem hiding this comment.
@AKDRG customers can use it since we'll support it as a state. Original intention was undocumented state, but it's documented and in arg spec now.
| - V(deleted) removes specified VRFs (or all if config is empty). | ||
| - V(gathered) returns current VRF state (the only state allowed on child | ||
| fabrics when targeted directly). | ||
| - V(staged) is an internal/private workflow state. It follows |
There was a problem hiding this comment.
I am a bit confused on what staged is supposed to be doing, could you clarify my overall understanding which might be wrong.
Should state: staged use full overridden attachment semantics while running definition CRUD as replaced and suppressing deployment, so that:
- Removing an attachment from a VRF that remains in config submits an undeployed
attach:false; the current code appears to skip this attachment removal. - Omitting an existing VRF entirely submits undeployed detach requests for its current attachments while retaining the VRF definition; the current code appears to handle this case as expected.
- Adding an attachment to an existing VRF submits an undeployed
attach:true; the current code appears to skip this attachment addition. - Creating a new VRF with attachments creates the VRF definition and submits undeployed
attach:truerequests; the current code appears to create the definition but skip its attachments.
It appears to me we that we do not test all of these scenarios but could have missed this.
There was a problem hiding this comment.
Hey Akini,
Thanks a lot for your feedback and testing. You had identified a critical bug here. I have fixed the gaps that were left out due to my commit miss and we are fully covered now.
- Removing an attachment from a VRF that remains in config submits an undeployed attach:false; the current code appears to skip this attachment removal.
Not skipped, attachment is removed with attach:false. Not un-deployed -> Staged.
- Adding an attachment to an existing VRF submits an undeployed attach:true; the current code appears to skip this attachment addition.
Not skipped, attachment is posted with attach:true. Not deployed -> Staged.
- Creating a new VRF with attachments creates the VRF definition and submits undeployed attach:true requests; the current code appears to create the definition but skip its attachments.
Not skipped, attachment is posted with attach:true. Not deployed -> Staged.
Added the integration tests too!
Thanks,
Akshay
636838b to
0bcb7b5
Compare
allenrobel
left a comment
There was a problem hiding this comment.
Code review
Four findings, all on the staged workflow plumbing in vrf_state_machine.py (the first is a functional bug; the network-side twin of each is noted inline where it applies).
🤖 Generated with Claude Code
| omitted_vrf_names, | ||
| attachment_details=omitted_attachment_details, | ||
| ) | ||
| if module_args.get("state") == "staged": |
There was a problem hiding this comment.
This staged short-circuit comes after the unconditional self.coordinator._ensure_vrfs_have_no_networks(...) call at the top of _prepare_overridden_deletions (line 378). That guard exists to "Fail before VRF deletion when networks still reference the VRFs" (vrf_dependency_checker.py::ensure_no_networks) and calls fail_json("Cannot delete VRF(s) because network(s) still reference them ...").
Since staged never deletes ("omitted VRFs are detached but not removed"), a staged task that omits a VRF still carrying networks hard-fails with a deletion error instead of performing the legitimate detach-only reconciliation. Suggest skipping the dependency check when module_args.get("state") == "staged", i.e. hoisting this guard above the _ensure_vrfs_have_no_networks call.
Note the new unit test (test_vrf_staged_detaches_omitted_vrfs_without_running_overridden_crud_delete) stubs _ensure_vrfs_have_no_networks as a call logger, so this path is not exercised by the added tests. The network twin (_ensure_networks_have_no_networks) is currently a no-op placeholder, so only the VRF side fails today — but the same reordering there would keep the two symmetric if that checker is ever implemented.
| @staticmethod | ||
| def _crud_module_args(module_args: dict) -> dict: | ||
| """Return module args for the generic CRUD state machine.""" | ||
| if module_args.get("state") != "staged": | ||
| return module_args | ||
| crud_args = dict(module_args) | ||
| crud_args["state"] = "replaced" | ||
| return crud_args | ||
|
|
||
| @staticmethod | ||
| def _query_module_args(module_args: dict) -> dict: | ||
| """Return module args for the current-state query phase.""" | ||
| if module_args.get("state") != "staged": | ||
| return module_args | ||
| query_args = dict(module_args) | ||
| query_args["state"] = "overridden" | ||
| return query_args | ||
|
|
||
| @staticmethod | ||
| def _prepare_crud_state(sm: Any, requested_state: str) -> None: | ||
| """Switch staged workflows to replacement CRUD after query.""" | ||
| if requested_state != "staged": | ||
| return | ||
| sm.state = "replaced" |
There was a problem hiding this comment.
These three helpers (_crud_module_args, _query_module_args, _prepare_crud_state) are byte-for-byte identical to the copies added in network_state_machine.py (lines 293-316), and the two classes share no base. This is the same two-independent-copies-may-drift situation raised on PR #505, which was resolved there by extracting the shared attachment_vpc_peer_expander.py. Consider the same treatment here — a small shared helper module or mixin — so a future change to the staged state mapping lands in one place instead of two.
| desired_vrf_names = self.coordinator._configured_vrf_names(config) | ||
|
|
||
| sm, original_config, original_state = self.coordinator._new_state_machine(module_args, strategy) | ||
| sm, original_config, original_state = self.coordinator._new_state_machine(self._query_module_args(module_args), strategy) |
There was a problem hiding this comment.
Because _query_module_args remaps state to "overridden" before _new_state_machine constructs the NDStateMachine, results.state (documented in rest/results.py as "The Ansible state for the current task") is captured as "overridden" for the whole staged run, and every API call's metadata["state"] in the -vvv api_metadata output reports overridden rather than staged. Metadata-only (requests, diff, and changed are unaffected), but it will mislead anyone debugging a staged task from verbose output. Worth restoring the user-requested state on sm.results after construction (e.g. in _prepare_crud_state). Same applies to the network twin.
| sm, original_config, original_state = self.coordinator._new_state_machine(module_args, strategy) | ||
| sm, original_config, original_state = self.coordinator._new_state_machine(self._query_module_args(module_args), strategy) | ||
| try: | ||
| self._prepare_crud_state(sm, state) |
There was a problem hiding this comment.
Minor/latent: NDStateMachine.__init__ threads context={"state": self.state} into pydantic validation "so models can apply state-aware validation", so sm.proposed here was validated under "overridden"; _prepare_crud_state then flips sm.state to "replaced" without re-validating. No current VRF/Network model reads the validation context, so this has no effect today — flagging it so a future state-aware validator doesn't silently validate staged runs under the wrong state. A comment noting the assumption would be enough.
Related Issue(s)
Related to:
Proposed Changes
Add a private/internal
_stagedworkflow state fornd_manage_vrfsandnd_manage_networks.The new workflow supports staged configuration removal where omitted resources should have attachments detached, but the omitted VRFs or Networks themselves should not be deleted.
Key behavior:
_stagedfollowsoverriddenattachment scope.replaced, preventing overridden-style delete requests._staged.Test Notes
Commands run:
PYTHONPATH=/Users/achengam/Documents/Ansible_Dev/NDNetwork_empty_interfaces
/Users/achengam/.pyenv/versions/ndfclab/bin/python
-m pytest tests/unit/module_utils/orchestrators/test_networks.py -k staged -q
Result:
2 passed, 80 deselected
PYTHONPATH=/Users/achengam/Documents/Ansible_Dev/NDNetwork_empty_interfaces
/Users/achengam/.pyenv/versions/ndfclab/bin/python
-m pytest tests/unit/module_utils/orchestrators/test_vrf_workflow_coordinator.py -k staged -q
Result:
2 passed, 54 deselected
black -l 159 --check
plugins/module_utils/orchestrators/network_attachment_manager.py
plugins/module_utils/orchestrators/network_state_machine.py
plugins/module_utils/orchestrators/vrf_attachment_manager.py
plugins/module_utils/orchestrators/vrf_state_machine.py
plugins/modules/nd_manage_networks.py
plugins/modules/nd_manage_vrfs.py
tests/unit/module_utils/orchestrators/test_networks.py
tests/unit/module_utils/orchestrators/test_vrf_workflow_coordinator.py
Result:
8 files would be left unchanged.
Cisco Nexus Dashboard Version
Workflow-layer change. No new API endpoint schema dependency introduced.
Related ND API Resource Category
Checklist