Skip to content

Ansible ND 4.X | WIP | ND Manage Policies Module + Pydantic Models + Smart Endpoints #261

Open
nikhilsrikrishna wants to merge 3 commits intoCiscoDevNet:developfrom
nikhilsrikrishna:policy_develop_pr
Open

Ansible ND 4.X | WIP | ND Manage Policies Module + Pydantic Models + Smart Endpoints #261
nikhilsrikrishna wants to merge 3 commits intoCiscoDevNet:developfrom
nikhilsrikrishna:policy_develop_pr

Conversation

@nikhilsrikrishna
Copy link
Copy Markdown
Collaborator

This PR adds the nd_policy module for switch policy management in the cisco.nd collection, including the core resource handler, endpoint wrappers, and Pydantic models.

What's Included

  • Policy resource handlernd_policy_resources.py supports gathered, merged, and deleted states. Uses bulk create, bulk delete with 207 response handling, the mark-delete → push-config → remove pipeline, and deploy through switch actions.

  • Endpoint definitions — Three endpoint classes: manage_fabrics_policies.py, manage_fabrics_policy_actions.py, manage_fabrics_switch_actions.py.

  • Pydantic model layer — Model classes: config_models.py, gathered_models.py, policy_actions.py, policy_base.py, policy_crud.py. These cover input validation, API response parsing, and gathered output formatting.

  • Input aliasingswitch_id is aliased as switch_ip in the argument spec, allowing users to specify either name interchangeably. Pydantic validators normalize both to the field expected by the API.

  • Template inputs validation — User-provided template_inputs are validated at runtime against the template's parameter schema fetched from the controller, checking for unknown keys, missing required parameters, and basic type correctness. System-injected keys are stripped from gathered output so only user-defined variables are returned.

  • Unit tests — Endpoint-level tests covering all three endpoint classes.

Notes

  • Delete workflow — Delete follows a 3-step flow: markDelete → pushConfig → remove. PYTHON content-type templates (e.g., switch_freeform, Ext_VRF_Lite_SVI) fail on markDelete with "Content type is PYTHON, cannot mark for deletion". Instead of maintaining a hardcoded list of these template names, the module inspects the 207 response — any policy that fails with this specific message is automatically retried via direct DELETE /policies/{policyId}, then deployed via switchActions/deploy to push the config removal to the switch.

  • Gathered state and policy_id — The gathered output includes a policy_id field (e.g., POLICY-28440) alongside the template name. When this output is fed back into state=merged, the policy_id can be directly used to identify the exact policy.

Work In Progress

  • ND output format structure
  • Integration tests

Copy link
Copy Markdown
Collaborator

@allenrobel allenrobel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting a few changes.

Comment thread plugins/module_utils/endpoints/v1/manage/manage_config_templates.py Outdated
Comment thread plugins/module_utils/endpoints/v1/manage/manage_config_templates.py
Comment thread plugins/module_utils/endpoints/v1/manage/manage_config_templates.py Outdated
Comment thread plugins/module_utils/endpoints/v1/manage/manage_config_templates.py
Comment thread plugins/module_utils/endpoints/v1/manage/manage_fabrics_policies.py
``config[]`` schema exactly for copy-paste round-trips.
"""

from __future__ import absolute_import, annotations, division, print_function
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.

Replace with:

from __future__ annotations

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Replaced the imports, Thank you

Comment thread plugins/module_utils/models/manage_policies/gathered_models.py Outdated
Comment thread plugins/module_utils/nd_policy_resources.py
Comment thread plugins/module_utils/nd_policy_resources.py Outdated
Comment thread plugins/module_utils/nd_policy_resources.py Outdated
Copy link
Copy Markdown
Collaborator

@allenrobel allenrobel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added several comments where legacy annotations are still being used. Please review your files more closely to remove remaining. Of you're using Claude Code, just ask Claude to update his memory, or update his Claude.md file, to use modern type annotations in all future code. You can also ask him to cleanup existing code.

- POST /fabrics/{fabricName}/policyActions/remove - Remove policies in bulk
"""

from __future__ import absolute_import, annotations, division, print_function
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.

Replace with:

from __future__ import annotations

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you


__author__ = "L Nikhil Sri Krishna"

from typing import Literal, Optional
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.

Remove Optional import and replace use of Optional with modern equivalents e.g.:

Optional[str] -> str | None

Comment applies throughout.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you


model_config = ConfigDict(extra="forbid")

cluster_name: Optional[str] = Field(
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.

Should be:

    cluster_name: str | None = Field(

Same comment applies throughout.

If you're using Claude Code, ask Claude to update his memory, or his Claude.md, file to use modern type annotations in all new code so that he stops using legacy annotations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you

- POST /fabrics/{fabricName}/switchActions/deploy - Deploy config to switches
"""

from __future__ import absolute_import, annotations, division, print_function
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.

Replace with:

from __future__ import annotations

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you

default_factory=dict,
description="Name/value pairs passed to the policy template",
)
switch: Optional[List[PlaybookSwitchEntry]] = Field(
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.

    switch: list[PlaybookSwitchEntry] | None = Field(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you

return self

@classmethod
def get_argument_spec(cls) -> Dict[str, Any]:
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.

    def get_argument_spec(cls) -> dict[str, Any]:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you

INTERFACE = "interface"

@classmethod
def choices(cls) -> List[str]:
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.

    def choices(cls) -> list[str]:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you

raise ValueError(f"Invalid entity type: {value}. Valid options: {cls.choices()}")

@classmethod
def normalize(cls, value: Union[str, "PolicyEntityType", None]) -> "PolicyEntityType":
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.

    def normalize(cls, value: str | PolicyEntityType | None) -> PolicyEntityType:

Note, forward reference quotes are no longer needed since from __future__ import annotations converts all annotations to strings.

Same comment applies throughout.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Made the changes, Thank you!


from __future__ import absolute_import, annotations, division, print_function

__metaclass__ = type
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.

Remove, no longer needed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenrobel Removed in the latest commit. Thanks!

Copy link
Copy Markdown
Collaborator

@allenrobel allenrobel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after the previous comments were addressed.


force_show_run: bool | None = Field(
default=None,
description=("If true, Config compliance fetches the latest running config " "from the device. If false, uses the cached version."),
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.

seems something went wrong with copy pasting?

Suggested change
description=("If true, Config compliance fetches the latest running config " "from the device. If false, uses the cached version."),
description=("If true, Config compliance fetches the latest running config from the device. If false, uses the cached version."),

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.

3 participants