Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/module_utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@
"disable",
"restart",
"delete",
"deleted",
"update",
"merged",
"replaced",
"overridden",
)

INTERFACE_FLOW_RULES_TYPES_MAPPING = {"port_channel": "PORTCHANNEL", "physical": "PHYSICAL", "l3out_sub_interface": "L3_SUBIF", "l3out_svi": "SVI"}
Expand Down
113 changes: 70 additions & 43 deletions plugins/module_utils/nd.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ansible.module_utils.basic import json
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.six import PY3
from ansible.module_utils.six.moves import filterfalse
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.connection import Connection
Expand Down Expand Up @@ -73,53 +72,27 @@ def cmp(a, b):


def issubset(subset, superset):
"""Recurse through nested dictionary and compare entries"""
"""Recurse through a nested dictionary and check if it is a subset of another."""

# Both objects are the same object
if subset is superset:
return True

# Both objects are identical
if subset == superset:
return True

# Both objects have a different type
if isinstance(subset) is not isinstance(superset):
if type(subset) is not type(superset):
return False

if not isinstance(subset, dict):
if isinstance(subset, list):
return all(item in superset for item in subset)
return subset == superset

for key, value in subset.items():
# Ignore empty values
if value is None:
return True
continue

# Item from subset is missing from superset
if key not in superset:
return False

# Item has different types in subset and superset
if isinstance(superset.get(key)) is not isinstance(value):
return False
superset_value = superset.get(key)

# Compare if item values are subset
if isinstance(value, dict):
if not issubset(superset.get(key), value):
return False
elif isinstance(value, list):
try:
# NOTE: Fails for lists of dicts
if not set(value) <= set(superset.get(key)):
return False
except TypeError:
# Fall back to exact comparison for lists of dicts
diff = list(filterfalse(lambda i: i in value, superset.get(key))) + list(filterfalse(lambda j: j in superset.get(key), value))
if diff:
return False
elif isinstance(value, set):
if not value <= superset.get(key):
return False
else:
if not value == superset.get(key):
return False
if not issubset(value, superset_value):
return False

return True

Expand Down Expand Up @@ -210,6 +183,9 @@ def __init__(self, module):

# info output
self.previous = dict()
self.before = []
self.commands = []
self.after = []
self.proposed = dict()
self.sent = dict()
self.stdout = None
Expand Down Expand Up @@ -433,6 +409,7 @@ def exit_json(self, **kwargs):
if self.params.get("state") in ALLOWED_STATES_TO_APPEND_SENT_AND_PROPOSED:
if self.params.get("output_level") in ("debug", "info"):
self.result["previous"] = self.previous
self.result["before"] = self.before
# FIXME: Modified header only works for PATCH
if not self.has_modified and self.previous != self.existing:
self.result["changed"] = True
Expand All @@ -450,8 +427,10 @@ def exit_json(self, **kwargs):
if self.params.get("state") in ALLOWED_STATES_TO_APPEND_SENT_AND_PROPOSED:
self.result["sent"] = self.sent
self.result["proposed"] = self.proposed
self.result["commands"] = self.commands

self.result["current"] = self.existing
self.result["after"] = self.after

if self.module._diff and self.result.get("changed") is True:
self.result["diff"] = dict(
Expand All @@ -468,6 +447,7 @@ def fail_json(self, msg, **kwargs):
if self.params.get("state") in ALLOWED_STATES_TO_APPEND_SENT_AND_PROPOSED:
if self.params.get("output_level") in ("debug", "info"):
self.result["previous"] = self.previous
self.result["before"] = self.before
# FIXME: Modified header only works for PATCH
if not self.has_modified and self.previous != self.existing:
self.result["changed"] = True
Expand All @@ -486,8 +466,10 @@ def fail_json(self, msg, **kwargs):
if self.params.get("state") in ALLOWED_STATES_TO_APPEND_SENT_AND_PROPOSED:
self.result["sent"] = self.sent
self.result["proposed"] = self.proposed
self.result["commands"] = self.commands

self.result["current"] = self.existing
self.result["after"] = self.after

self.result.update(**kwargs)
self.module.fail_json(msg=msg, **self.result)
Expand All @@ -499,23 +481,30 @@ def check_changed(self):
existing["password"] = self.sent.get("password")
return not issubset(self.sent, existing)

def get_diff(self, unwanted=None):
def get_diff(self, unwanted=None, previous=None, payload=None):
"""Check if existing payload and sent payload and removing keys that are not required"""
if unwanted is None:
unwanted = []
if not self.existing and self.sent:
return True

if previous is None and payload is None:
if not self.existing and self.sent:
return True

existing = self.existing
sent = self.sent

if previous and payload:
existing = previous
sent = payload

for key in unwanted:
if isinstance(key, str):
if key in existing:
try:
del existing[key]
except KeyError:
pass
if key in sent:
try:
del sent[key]
except KeyError:
Copy link
Collaborator

Choose a reason for hiding this comment

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

You may not need this try/except around this anymore if key is being checked first.

Expand All @@ -524,15 +513,53 @@ def get_diff(self, unwanted=None):
key_path, last = key[:-1], key[-1]
try:
existing_parent = reduce(dict.get, key_path, existing)
del existing_parent[last]
if existing_parent is not None:
del existing_parent[last]
except KeyError:
pass
try:
sent_parent = reduce(dict.get, key_path, sent)
del sent_parent[last]
if sent_parent is not None:
del sent_parent[last]
except KeyError:
pass
return not issubset(sent, existing)

def set_to_empty_string_when_none(self, val):
return val if val is not None else ""

def get_object_by_nested_key_value(self, path, nested_key_path, value, data_key=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add a doc string explanation for this function?


response_data = self.request(path, method="GET")

if not response_data:
return None

object_list = []
if isinstance(response_data, list):
object_list = response_data
elif data_key and data_key in response_data:
object_list = response_data.get(data_key)
else:
return None

keys = nested_key_path.split(".")

for obj in object_list:
current_level = obj
for key in keys:
if isinstance(current_level, dict):
current_level = current_level.get(key)
else:
current_level = None
break

if current_level == value:
return obj

return None

def delete(self, check_mode, path):
if not check_mode:
self.request(path, method="DELETE")
return {"path": path, "method": "DELETE"}
67 changes: 67 additions & 0 deletions plugins/module_utils/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we want to start to write unit tests for all these newly introduced utils function?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, I think we should be adding unit tests for util functions like these.


# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) <[email protected]>
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function

__metaclass__ = type


def snake_to_camel(snake_str, upper_case_components=None):
if snake_str is not None and "_" in snake_str:
if upper_case_components is None:
upper_case_components = []
components = snake_str.split("_")
camel_case_str = components[0]

for component in components[1:]:
if component in upper_case_components:
camel_case_str += component.upper()
else:
camel_case_str += component.title()

return camel_case_str
else:
return snake_str


def compare_config_and_remote_objects(remote_objects, config_objects, key="name"):
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure I like the naming of remote_objects and config_objects, I would prefer something in the area of configured _objects and desired_objects ( not sure I would even like objects ).

remote_object_names = {obj[key] for obj in remote_objects}
config_object_names = {obj[key] for obj in config_objects}
Comment on lines +30 to +31
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we assume the key always exists or use a get() function to avoid key errors on wrong objects?


# Common objects from Config (name in both remote and config data)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this would be key, but I would argue as in other comment it would be identifier list

update = [obj for obj in config_objects if obj[key] in remote_object_names]

# Unmatched objects from Remote (name not in Config)
delete = [obj for obj in remote_objects if obj[key] not in config_object_names]

# Unmatched objects from Config (name not in Remote)
create = [obj for obj in config_objects if obj[key] not in remote_object_names]

return {
"config_data_update": update,
"remote_data_delete": delete, # Only when state is overridden
"config_data_create": create,
}


def compare_unordered_list_of_dicts(list1, list2):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this function also take into consideration that lists within the dictionaries could also be ordered different? Do we only need unordered comparison on the top level and not recursive?

if (not isinstance(list1, list) or not isinstance(list2, list)) or (len(list1) != len(list2)):
return False

for dict1 in list1:
found_match = False
for i, dict2 in enumerate(list2):
if dict1 == dict2:
list2.pop(i)
found_match = True
break
if not found_match:
return False

return True


def wrap_objects_by_key(object_list, key="name"):
return {obj.get(key): obj for obj in object_list}
Loading
Loading