Skip to content

Add shared module utility support#294

Open
sivakasi-cisco wants to merge 1 commit into
CiscoDevNet:developfrom
sivakasi-cisco:nd_vrf_lite_common_files
Open

Add shared module utility support#294
sivakasi-cisco wants to merge 1 commit into
CiscoDevNet:developfrom
sivakasi-cisco:nd_vrf_lite_common_files

Conversation

@sivakasi-cisco
Copy link
Copy Markdown
Collaborator

@sivakasi-cisco sivakasi-cisco commented May 27, 2026

Problem Description

This PR separates out a few shared framework changes that are needed before modules like nd_manage_vrf_lite can cleanly follow the generic ND 4.x module architecture - #281.

Some ND 4.x resources are simple: one playbook item maps directly to one controller object. In those cases, the existing generic state machine can compare “what the user wants” with “what exists on the controller” and decide create, update, delete, or no-op.

But some resources are nested. VRF Lite is a good example: the user gives VRF-level config, but the actual work happens under smaller child objects like VRF attachments and VRF Lite links. Without small shared framework improvements, each nested module has to write its own custom comparison logic.

This PR adds the common support needed for that style of module.

Why These Common Changes Are Needed

  • nd_state_machine.py now allows a module to normalize or flatten playbook config after reading current controller state. This lets nested modules convert user input into the smaller objects that the generic state machine can compare.
  • nd_state_machine.py also lets modules handle scoped delete behavior for replaced state. This is useful when only child objects under a specific parent should be removed.
  • nd_state_machine.py now tracks deleted items in sent, so later steps like save/deploy can know what was actually changed.
  • models/base.py and utils.py improve diff comparison so controller-returned extra fields do not cause false “changed” results when the user did not specify those fields.
  • common/data.py adds small reusable helpers for common controller response handling, JSON parsing, dict-list cleanup, object copying, and safe integer conversion.

items_to_delete = [existing_item for identifier in diff_identifiers if (existing_item := self.existing.get(identifier)) is not None]
self._delete_items(items_to_delete)

def _manage_replace_deletions(self) -> None:
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.

Kindly comment on "replace" state if it is okay

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
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.

what is the difference between data.py and the utils.py, they both seem helper functions is there a reason to add this into a separate file/location?

if value is None:
return None

text = str(value).strip()
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 we wrap this into a try/except to be sure the str() works?

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
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.

please add unit tests for all the helper functions


text = str(value).strip()
if not text:
return None
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.

why return None for an empty text (assuming here text is always a string) and not an empty string to preserve it's type?

return {}


def loads_maybe_json(value: Any) -> 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.

Why name the function loads_maybe_json when it could be json or python literal string? I think we should make this more naming more specific, the function more generic or the function more specific.

Personally i would make the function more generic and extensible by calling it parse_value, where the function takes a value and a list of parsers like json.loads and ast.literal_eval, this way if you want to extend parsing capabilities in the future you should be able to leverage the function.

Comment on lines +37 to +40
try:
return json.loads(text)
except Exception:
try:
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.

nested try/except are typically a code smell, think we should use a loop alternative

return None


def coerce_dict_list(data: Any, list_keys: tuple[str, ...] = ("DATA", "data", "items")) -> list[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.

should this function also support deeper levels of nesting?
can you document the restrictions of this function more clearly?

return []


def copy_dict_items(items: Any) -> list[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.

we should a stricter type hinting on items, probably something like Optional[Iterable[Any]] or Iterable[Mapping[str, Any] | BaseModel]

# Initialize collections
try:
response_data = self.model_orchestrator.query_all()
config_data = self.module.params.get("config", [])
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.

dict.get(key, default) only returns default when the key is missing. If config is explicitly present with value None, you get None back — not []. is this intended else change to below

Suggested change
config_data = self.module.params.get("config", [])
config_data = self.module.params.get("config") or []

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