-
Notifications
You must be signed in to change notification settings - Fork 19
Created nd_backup_schedule module using Ansible Network Resource Modules Approach (DCNE-496) #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
39dd8a5
955b9b1
5c22c2d
3cab814
a6465d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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( | ||
|
@@ -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 | ||
|
@@ -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) | ||
|
@@ -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: | ||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# -*- coding: utf-8 -*- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} |
There was a problem hiding this comment.
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.