diff --git a/plugins/modules/ipaautomember.py b/plugins/modules/ipaautomember.py index eb5dec19c..3565de927 100644 --- a/plugins/modules/ipaautomember.py +++ b/plugins/modules/ipaautomember.py @@ -217,7 +217,8 @@ """ from ansible.module_utils.ansible_freeipa_module import ( - IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, ipalib_errors, DN + IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, ipalib_errors, DN, + IPADiffTracker, gen_args_diff ) @@ -444,9 +445,9 @@ def main(): changed = False exit_args = {} res_find = None + diff_tracker = IPADiffTracker() with ansible_module.ipa_connect(): - commands = [] for name in names: @@ -459,7 +460,6 @@ def main(): # Check inclusive and exclusive conditions if inclusive is not None or exclusive is not None: - # automember_type is either "group" or "hostgorup" if automember_type == "group": _type = u"user" elif automember_type == "hostgroup": @@ -490,20 +490,23 @@ def main(): res_find, ignore=['type']): commands.append([name, 'automember_mod', args]) + before, after = gen_args_diff(args, res_find, ignore=['type']) + diff_tracker.add_entry_diff(name, before, after) else: commands.append([name, 'automember_add', args]) + diff_tracker.add_entry_diff(name, {}, args) res_find = {} if inclusive is not None: inclusive_add, inclusive_del = gen_add_del_lists( transform_conditions(inclusive), - res_find.get("automemberinclusiveregex", []) + (res_find or {}).get("automemberinclusiveregex", []) ) if exclusive is not None: exclusive_add, exclusive_del = gen_add_del_lists( transform_conditions(exclusive), - res_find.get("automemberexclusiveregex", []) + (res_find or {}).get("automemberexclusiveregex", []) ) elif action == "member": @@ -520,6 +523,14 @@ def main(): automember_type, key, inclusiveregex=regex) commands.append([name, 'automember_add_condition', condition_args]) + # For diff: show the regex being added + before_list = (res_find or {}).get("automemberinclusiveregex", []) + after_list = before_list + [f"{key}={regex}"] + diff_tracker.add_entry_diff( + name, + {"inclusive": before_list}, + {"inclusive": after_list} + ) for _inclusive in inclusive_del: key, regex = _inclusive.split("=", 1) @@ -527,6 +538,14 @@ def main(): automember_type, key, inclusiveregex=regex) commands.append([name, 'automember_remove_condition', condition_args]) + # For diff: show the regex being removed + before_list = (res_find or {}).get("automemberinclusiveregex", []) + after_list = [r for r in before_list if r != f"{key}={regex}"] + diff_tracker.add_entry_diff( + name, + {"inclusive": before_list}, + {"inclusive": after_list} + ) for _exclusive in exclusive_add: key, regex = _exclusive.split("=", 1) @@ -534,6 +553,13 @@ def main(): automember_type, key, exclusiveregex=regex) commands.append([name, 'automember_add_condition', condition_args]) + before_list = (res_find or {}).get("automemberexclusiveregex", []) + after_list = before_list + [f"{key}={regex}"] + diff_tracker.add_entry_diff( + name, + {"exclusive": before_list}, + {"exclusive": after_list} + ) for _exclusive in exclusive_del: key, regex = _exclusive.split("=", 1) @@ -541,12 +567,24 @@ def main(): automember_type, key, exclusiveregex=regex) commands.append([name, 'automember_remove_condition', condition_args]) + before_list = (res_find or {}).get("automemberexclusiveregex", []) + after_list = [r for r in before_list if r != f"{key}={regex}"] + diff_tracker.add_entry_diff( + name, + {"exclusive": before_list}, + {"exclusive": after_list} + ) elif state == 'absent': if action == "automember": if res_find is not None: commands.append([name, 'automember_del', {'type': automember_type}]) + diff_tracker.add_entry_diff( + name, + {"state": "present"}, + {"state": "absent"} + ) elif action == "member": if res_find is None: @@ -561,6 +599,13 @@ def main(): commands.append( [name, 'automember_remove_condition', condition_args]) + before_list = (res_find or {}).get("automemberinclusiveregex", []) + after_list = [r for r in before_list if r != f"{key}={regex}"] + diff_tracker.add_entry_diff( + name, + {"inclusive": before_list}, + {"inclusive": after_list} + ) if exclusive is not None: for _exclusive in transform_conditions(exclusive): @@ -570,6 +615,13 @@ def main(): commands.append([name, 'automember_remove_condition', condition_args]) + before_list = (res_find or {}).get("automemberexclusiveregex", []) + after_list = [r for r in before_list if r != f"{key}={regex}"] + diff_tracker.add_entry_diff( + name, + {"exclusive": before_list}, + {"exclusive": after_list} + ) if len(names) == 0: if state == "rebuilt": @@ -580,7 +632,7 @@ def main(): elif state == "orphans_removed": res_find = find_automember_orphans(ansible_module, automember_type) - if res_find["count"] > 0: + if (res_find or {}).get("count", 0) > 0: commands.append([None, 'automember_find_orphans', {'type': automember_type, 'remove': True}]) @@ -590,28 +642,37 @@ def main(): automember_type) if default_group == "": - if isinstance(res_find["automemberdefaultgroup"], list): + if isinstance((res_find or {}).get("automemberdefaultgroup", None), list): commands.append([None, 'automember_default_group_remove', {'type': automember_type}]) + diff_tracker.add_entry_diff( + None, + {"default_group": (res_find or {}).get("automemberdefaultgroup", [])}, + {"default_group": []} + ) else: dn_default_group = [DN(('cn', default_group), ('cn', '%ss' % automember_type), ('cn', 'accounts'), ansible_module.ipa_get_basedn())] - if repr(res_find["automemberdefaultgroup"]) != \ + if repr((res_find or {}).get("automemberdefaultgroup", None)) != \ repr(dn_default_group): commands.append( [None, 'automember_default_group_set', {'type': automember_type, 'automemberdefaultgroup': default_group}]) + diff_tracker.add_entry_diff( + None, + {"default_group": (res_find or {}).get("automemberdefaultgroup", None)}, + {"default_group": dn_default_group} + ) else: ansible_module.fail_json(msg="Invalid operation") # Execute commands - changed = ansible_module.execute_ipa_commands(commands) # result["failed"] is used only for INCLUDE_RE, EXCLUDE_RE @@ -622,7 +683,8 @@ def main(): # in other modules. # Done - ansible_module.exit_json(changed=changed, **exit_args) + _exit_kwargs = dict(exit_args, **diff_tracker.build_diff()) + ansible_module.exit_json(changed=changed, **_exit_kwargs) if __name__ == "__main__": diff --git a/plugins/modules/ipadnsconfig.py b/plugins/modules/ipadnsconfig.py index a8ae39592..1432237f7 100644 --- a/plugins/modules/ipadnsconfig.py +++ b/plugins/modules/ipadnsconfig.py @@ -120,7 +120,7 @@ """ from ansible.module_utils.ansible_freeipa_module import \ - IPAAnsibleModule, compare_args_ipa, is_ipv4_addr, is_ipv6_addr + IPAAnsibleModule, compare_args_ipa, is_ipv4_addr, is_ipv6_addr, IPADiffTracker, gen_args_diff def find_dnsconfig(module): @@ -242,30 +242,31 @@ def main(): # Init + changed = False + diff_tracker = IPADiffTracker() # Connect to IPA API with ansible_module.ipa_connect(): - res_find = find_dnsconfig(ansible_module) args = gen_args(ansible_module, state, action, res_find, forwarders, forward_policy, allow_sync_ptr) # Execute command only if configuration changes. if not compare_args_ipa(ansible_module, args, res_find): + before, after = gen_args_diff(args, res_find) + diff_tracker.add_entry_diff("dnsconfig", before, after) try: if not ansible_module.check_mode: ansible_module.ipa_command_no_name('dnsconfig_mod', args) - # If command did not fail, something changed. changed = True - except Exception as e: msg = str(e) ansible_module.fail_json(msg="dnsconfig_mod: %s" % msg) # Done - - ansible_module.exit_json(changed=changed) + _exit_kwargs = dict(diff_tracker.build_diff()) + ansible_module.exit_json(changed=changed, **_exit_kwargs) if __name__ == "__main__": diff --git a/plugins/modules/ipadnsrecord.py b/plugins/modules/ipadnsrecord.py index 06ab43da8..0e2abbe92 100644 --- a/plugins/modules/ipadnsrecord.py +++ b/plugins/modules/ipadnsrecord.py @@ -966,7 +966,8 @@ from ansible.module_utils._text import to_text from ansible.module_utils.ansible_freeipa_module import \ - IPAAnsibleModule, is_ipv4_addr, is_ipv6_addr, ipalib_errors + IPAAnsibleModule, is_ipv4_addr, is_ipv6_addr, ipalib_errors, \ + IPADiffTracker, gen_args_diff try: import dns.reversename import dns.resolver @@ -1586,6 +1587,7 @@ def main(): changed = False exit_args = {} + diff_tracker = IPADiffTracker() # Connect to IPA API with ansible_module.ipa_connect(): @@ -1608,13 +1610,45 @@ def main(): res_find = find_dnsrecord(ansible_module, zone_name, name) cmds = [] + diff_key = "%s/%s" % (zone_name, name) if state == 'present': cmds = define_commands_for_present_state( ansible_module, zone_name, entry, res_find) + if cmds: + args = gen_args(entry) + if res_find is None: + diff_tracker.add_entry_diff(diff_key, {}, args) + else: + before, after = gen_args_diff( + args, res_find, ignore=['idnsname']) + diff_tracker.add_entry_diff(diff_key, before, after) elif state == 'absent': cmds = define_commands_for_absent_state( ansible_module, zone_name, entry, res_find) + if cmds: + args = gen_args(entry) + if args.get('del_all', False): + diff_tracker.add_entry_diff( + diff_key, + {"state": "present"}, {"state": "absent"}) + else: + records_to_delete = { + k: v for k, v in args.items() + if k.endswith('record') + } + actually_removed = {} + if res_find: + for rec, values in records_to_delete.items(): + del_list = [ + v for v in values + if rec in res_find and v in res_find[rec] + ] + if del_list: + actually_removed[rec] = del_list + if actually_removed: + diff_tracker.add_entry_diff( + diff_key, actually_removed, {}) else: ansible_module.fail_json(msg="Unkown state '%s'" % state) @@ -1626,7 +1660,8 @@ def main(): commands, exception_handler=exception_handler) # Done - ansible_module.exit_json(changed=changed, host=exit_args) + _exit_kwargs = dict(host=exit_args, **diff_tracker.build_diff()) + ansible_module.exit_json(changed=changed, **_exit_kwargs) if __name__ == "__main__": diff --git a/plugins/modules/ipadnszone.py b/plugins/modules/ipadnszone.py index 78002b3f4..601c5e30d 100644 --- a/plugins/modules/ipadnszone.py +++ b/plugins/modules/ipadnszone.py @@ -231,7 +231,9 @@ compare_args_ipa, IPAParamMapping, DNSName, - netaddr + netaddr, + IPADiffTracker, + gen_args_diff, ) # noqa: E402 from ansible.module_utils import six @@ -276,6 +278,7 @@ def __init__(self, *args, **kwargs): self.commands = [] self.ipa_params = IPAParamMapping(self, ipa_param_mapping) self.exit_args = {} + self.diff_tracker = IPADiffTracker() def validate_ips(self, ips, error_msg): invalid_ips = [ @@ -493,6 +496,7 @@ def define_ipa_commands(self): # Since the zone doesn't exist we just create it # with given args self.commands.append((zone_name, "dnszone_add", args)) + self.diff_tracker.add_entry_diff(zone_name, {}, args) is_zone_active = True # just_added = True @@ -501,6 +505,9 @@ def define_ipa_commands(self): # matches the current config. If not we updated it. if not compare_args_ipa(self, args, zone): self.commands.append((zone_name, "dnszone_mod", args)) + before, after = gen_args_diff(args, zone) + self.diff_tracker.add_entry_diff( + zone_name, before, after) # Permissions must be set on existing zones. if self.ipa_params.permission is not None: @@ -508,18 +515,38 @@ def define_ipa_commands(self): if self.ipa_params.permission and not is_managed: self.commands.append( (zone_name, "dnszone_add_permission", {})) + self.diff_tracker.add_entry_diff( + zone_name, + {"permission": False}, + {"permission": True}) if not self.ipa_params.permission and is_managed: self.commands.append( (zone_name, "dnszone_remove_permission", {})) + self.diff_tracker.add_entry_diff( + zone_name, + {"permission": True}, + {"permission": False}) if self.ipa_params.state == "enabled" and not is_zone_active: self.commands.append((zone_name, "dnszone_enable", {})) + self.diff_tracker.add_entry_diff( + zone_name, + {"state": "disabled"}, + {"state": "enabled"}) if self.ipa_params.state == "disabled" and is_zone_active: self.commands.append((zone_name, "dnszone_disable", {})) + self.diff_tracker.add_entry_diff( + zone_name, + {"state": "enabled"}, + {"state": "disabled"}) if self.ipa_params.state == "absent" and zone is not None: self.commands.append((zone_name, "dnszone_del", {})) + self.diff_tracker.add_entry_diff( + zone_name, + {"state": "present"}, + {"state": "absent"}) def process_results(self, _result, command, name, _args, exit_args): if command == "dnszone_add" and self.ipa_params.name_from_ip: @@ -605,7 +632,8 @@ def main(): result_handler=DNSZoneModule.process_results, exit_args=exit_args ) - ansible_module.exit_json(changed=changed, **exit_args) + _exit_kwargs = dict(exit_args, **ansible_module.diff_tracker.build_diff()) + ansible_module.exit_json(changed=changed, **_exit_kwargs) if __name__ == "__main__": diff --git a/plugins/modules/iparole.py b/plugins/modules/iparole.py index 09c407ab8..9ab5cc1b9 100644 --- a/plugins/modules/iparole.py +++ b/plugins/modules/iparole.py @@ -134,7 +134,7 @@ from ansible.module_utils._text import to_text from ansible.module_utils.ansible_freeipa_module import \ IPAAnsibleModule, gen_add_del_lists, compare_args_ipa, \ - gen_intersection_list, ensure_fqdn, ipalib_errors + gen_intersection_list, ensure_fqdn, ipalib_errors, IPADiffTracker, gen_args_diff from ansible.module_utils import six if six.PY3: @@ -468,9 +468,9 @@ def main(): # Init + diff_tracker = IPADiffTracker() # Connect to IPA API with ansible_module.ipa_connect(): - state = ansible_module.params_get("state") action = ansible_module.params_get("action") names = ansible_module.params_get("name") @@ -484,18 +484,61 @@ def main(): msg="sysaccount members are not supported by your IPA version") for name in names: + res_find = find_role(ansible_module, name) + args = gen_args(ansible_module) + # Diff for role add + if state == "present" and action == "role": + if res_find is None: + diff_tracker.add_entry_diff(name, {}, args) + elif not compare_args_ipa(ansible_module, args, res_find): + before, after = gen_args_diff(args, res_find) + diff_tracker.add_entry_diff(name, before, after) + # Diff for role rename + if state == "renamed": + if res_find is not None: + before, after = gen_args_diff(args, res_find) + diff_tracker.add_entry_diff(name, before, after) + # Diff for role delete + if state == "absent" and action == "role" and res_find is not None: + diff_tracker.add_entry_diff(name, res_find, {}) + # Diff for member add/remove + # Only for present/absent and action=member + if action == "member" and res_find is not None: + # privilege + param = ansible_module.params_get_lowercase("privilege") + if param is not None: + before = set(result_get_value_lowercase(res_find, "memberof_privilege", [])) + after = set(param) + if before != after: + diff_tracker.add_entry_diff(f"{name}/privilege", list(before), list(after)) + # user, group, host, hostgroup, sysaccount + for key in ["user", "group", "host", "hostgroup", "sysaccount"]: + param = ansible_module.params_get_lowercase(key) + if param is not None: + before = set(result_get_value_lowercase(res_find, f"member_{key}", [])) + after = set(param) + if before != after: + diff_tracker.add_entry_diff(f"{name}/{key}", list(before), list(after)) + # service + param = get_service_param(ansible_module, "service") + if param is not None: + before = set(result_get_value_lowercase(res_find, "member_service", [])) + after = set(param.keys()) + if before != after: + diff_tracker.add_entry_diff(f"{name}/service", list(before), list(after)) + cmds = role_commands_for_name(ansible_module, state, action, name) commands.extend(cmds) exit_args = {} # Execute commands - changed = ansible_module.execute_ipa_commands( commands, fail_on_member_errors=True) # Done - ansible_module.exit_json(changed=changed, **exit_args) + _exit_kwargs = dict(exit_args, **diff_tracker.build_diff()) + ansible_module.exit_json(changed=changed, **_exit_kwargs) if __name__ == "__main__":