From 917b3b62c7c1850eb6e77cab428ceaf90cbe066d Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Tue, 7 Sep 2021 11:12:02 +0200 Subject: [PATCH 01/22] IPAAnsibleModule: New staticethod member_error_handler The staticmethod member_error_handler is handing the default member related failures that can occur for modules with member support. This can simply be enabled with fail_on_member_errors=True for execute_ipa_commands. An exception handler is also now usable with execute_ipa_commands. In addition to the the exception it is also getting the same user defined arguments that the result_handler is getting. handle_result has been renamed in result_handler and handle_result_user_args has been renamed to handlers_user_args. Additionally the errors list does not need to be defined in the module. The method execute_ipa_commands is doing this internally and is also adding error: error to handlers_user_args if the handler is having errors in the argspec and errors is not yet set in handlers_user_args. Tests have been added to make sure that no user args for the handler have been set without an own result or exception handler. Also the use of fail_on_member_errors together with a result_andler is leading to an error. --- .../module_utils/ansible_freeipa_module.py | 103 +++++++++++++++--- 1 file changed, 85 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 45e956d0..45eabb88 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -45,6 +45,7 @@ else: import gssapi from datetime import datetime from contextlib import contextmanager + import inspect # ansible-freeipa requires locale to be C, IPA requires utf-8. os.environ["LANGUAGE"] = "C" @@ -716,8 +717,21 @@ else: """ return api_check_ipa_version(oper, requested_version) - def execute_ipa_commands(self, commands, handle_result=None, - **handle_result_user_args): + # pylint: disable=unused-argument + @staticmethod + def member_error_handler(module, result, command, name, args, errors): + # Get all errors + for failed_item in result.get("failed", []): + failed = result["failed"][failed_item] + for member_type in failed: + for member, failure in failed[member_type]: + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + def execute_ipa_commands(self, commands, result_handler=None, + exception_handler=None, + fail_on_member_errors=False, + **handlers_user_args): """ Execute IPA API commands from command list. @@ -727,30 +741,56 @@ else: The list of commands in the form (name, command and args) For commands that do not require a 'name', None needs be used. - handle_result: function + result_handler: function The user function to handle results of the single commands - handle_result_user_args: dict (user args mapping) - The user args to pass to handle_result function + exception_handler: function + The user function to handle exceptions of the single commands + Returns True to continue to next command, else False + fail_on_member_errors: bool + Use default member error handler handler member_error_handler + handlers_user_args: dict (user args mapping) + The user args to pass to result_handler and exception_handler + functions Example (ipauser module): - def handle_result(module, result, command, name, args, exit_args): + def result_handler(module, result, command, name, args, exit_args, + one_name): if "random" in args and command in ["user_add", "user_mod"] \ and "randompassword" in result["result"]: - exit_args.setdefault(name, {})["randompassword"] = \ - result["result"]["randompassword"] + if one_name: + exit_args["randompassword"] = \ + result["result"]["randompassword"] + else: + exit_args.setdefault(name, {})["randompassword"] = \ + result["result"]["randompassword"] + + def exception_handler(module, ex, exit_args, one_name): + if ex.exception == ipalib_errors.EmptyModlist: + result = {} + return False exit_args = {} - changed = module.execute_ipa_commands(commands, handle_result, - exit_args=exit_args) + changed = module.execute_ipa_commands(commands, result_handler, + exception_handler, + exit_args=exit_args, + one_name=len(names)==1) - if len(names) == 1: - ansible_module.exit_json(changed=changed, - user=exit_args[names[0]]) - else: - ansible_module.exit_json(changed=changed, user=exit_args) + ansible_module.exit_json(changed=changed, user=exit_args) """ + # Fail on given handlers_user_args without result or exception + # handler + if result_handler is None and exception_handler is None and \ + len(handlers_user_args) > 0: + self.fail_json(msg="Args without result and exception hander: " + "%s" % repr(handlers_user_args)) + + # Fail on given result_handler and fail_on_member_errors + if result_handler is not None and fail_on_member_errors: + self.fail_json( + msg="result_handler given and fail_on_member_errors set") + # No commands, report no changes if commands is None: return False @@ -759,6 +799,24 @@ else: if self.check_mode: return len(commands) > 0 + # Error list for result_handler and error_handler + _errors = [] + + # Handle fail_on_member_errors, set result_handler to + # member_error_handler + # Add internal _errors for result_hendler if the module is not + # adding it. This also activates the final fail_json if + # errors are found. + if fail_on_member_errors: + result_handler = IPAAnsibleModule.member_error_handler + handlers_user_args["errors"] = _errors + elif result_handler is not None: + if "errors" not in handlers_user_args: + # pylint: disable=deprecated-method + argspec = inspect.getargspec(result_handler) + if "errors" in argspec.args: + handlers_user_args["errors"] = _errors + changed = False for name, command, args in commands: try: @@ -773,13 +831,22 @@ else: else: changed = True - if handle_result is not None: - handle_result(self, result, command, name, args, - **handle_result_user_args) + # If result_handler is not None, call it with user args + # defined in **handlers_user_args + if result_handler is not None: + result_handler(self, result, command, name, args, + **handlers_user_args) except Exception as e: + if exception_handler is not None and \ + exception_handler(self, e, **handlers_user_args): + continue self.fail_json(msg="%s: %s: %s" % (command, name, str(e))) + # Fail on errors from result_handler and exception_handler + if len(_errors) > 0: + self.fail_json(msg=", ".join(_errors)) + return changed class FreeIPABaseModule(IPAAnsibleModule): From e6f0eb23955f724e83bb1749e244e33d16e069d9 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:44:15 +0200 Subject: [PATCH 02/22] automember: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipaautomember.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/plugins/modules/ipaautomember.py b/plugins/modules/ipaautomember.py index 49c8d9a2..9a93cd23 100644 --- a/plugins/modules/ipaautomember.py +++ b/plugins/modules/ipaautomember.py @@ -390,32 +390,16 @@ def main(): commands.append([None, 'automember_rebuild', {"hosts": rebuild_hosts}]) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) + # Execute commands - for name, command, args in commands: - try: - if name is None: - result = ansible_module.ipa_command_no_name(command, args) - else: - result = ansible_module.ipa_command(command, name, args) + changed = ansible_module.execute_ipa_commands(commands) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as ex: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(ex))) - - # result["failed"] is used only for INCLUDE_RE, EXCLUDE_RE - # if entries could not be added that are already there and - # it entries could not be removed that are not there. - # All other issues like invalid attributes etc. are handled - # as exceptions. Therefore the error section is not here as - # in other modules. + # result["failed"] is used only for INCLUDE_RE, EXCLUDE_RE + # if entries could not be added that are already there and + # if entries could not be removed that are not there. + # All other issues like invalid attributes etc. are handled + # as exceptions. Therefore the error section is not here as + # in other modules. # Done ansible_module.exit_json(changed=changed, **exit_args) From 95ffd2c5a64dc02eaae39c6989132e6bb59d78a5 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:48:18 +0200 Subject: [PATCH 03/22] delegation: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipadelegation.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/plugins/modules/ipadelegation.py b/plugins/modules/ipadelegation.py index 846e1277..3ebbe88c 100644 --- a/plugins/modules/ipadelegation.py +++ b/plugins/modules/ipadelegation.py @@ -289,23 +289,9 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From 190737302324b54d88fef262db5077b6ec3b91a0 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:49:31 +0200 Subject: [PATCH 04/22] group: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipagroup.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/plugins/modules/ipagroup.py b/plugins/modules/ipagroup.py index c19644c2..bbc8e973 100644 --- a/plugins/modules/ipagroup.py +++ b/plugins/modules/ipagroup.py @@ -658,34 +658,10 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) - # Get all errors - # result are ignored. All others are reported. - errors = [] - for failed_item in result.get("failed", []): - failed = result["failed"][failed_item] - for member_type in failed: - for member, failure in failed[member_type]: - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands( + commands, fail_on_member_errors=True) # Done From 928deb2176cc35fc3ca82c9f8b91cdb4aa37ab34 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:50:35 +0200 Subject: [PATCH 05/22] hbacrule: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipahbacrule.py | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/plugins/modules/ipahbacrule.py b/plugins/modules/ipahbacrule.py index ea8fd73a..1d6a3b2f 100644 --- a/plugins/modules/ipahbacrule.py +++ b/plugins/modules/ipahbacrule.py @@ -595,35 +595,10 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - errors = [] - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) - # Get all errors - # result are ignored. All others are reported. - if "failed" in result and len(result["failed"]) > 0: - for item in result["failed"]: - failed_item = result["failed"][item] - for member_type in failed_item: - for member, failure in failed_item[member_type]: - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands( + commands, fail_on_member_errors=True) # Done From 2c96d91d4c4b0f266e603cc3f4b6346d9a8fb8dd Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:51:38 +0200 Subject: [PATCH 06/22] hbacsvcgroup: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipahbacsvcgroup.py | 46 +++++++++++------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/plugins/modules/ipahbacsvcgroup.py b/plugins/modules/ipahbacsvcgroup.py index 5e301d6a..1e6e3439 100644 --- a/plugins/modules/ipahbacsvcgroup.py +++ b/plugins/modules/ipahbacsvcgroup.py @@ -136,6 +136,21 @@ def gen_member_args(hbacsvc): return _args +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors): + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and "member" in result["failed"]: + failed = result["failed"]["member"] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" not in failure \ + and "not a member" not in failure: + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + def main(): ansible_module = IPAAnsibleModule( argument_spec=dict( @@ -278,36 +293,9 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - errors = [] - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - if "failed" in result and "member" in result["failed"]: - failed = result["failed"]["member"] - for member_type in failed: - for member, failure in failed[member_type]: - if "already a member" not in failure \ - and "not a member" not in failure: - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + + changed = ansible_module.execute_ipa_commands(commands, result_handler) # Done From 6ae68980e881db21d6991b63b6190866bc083e97 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:52:37 +0200 Subject: [PATCH 07/22] hbacsvc: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipahbacsvc.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/plugins/modules/ipahbacsvc.py b/plugins/modules/ipahbacsvc.py index c6e31d47..12c8476d 100644 --- a/plugins/modules/ipahbacsvc.py +++ b/plugins/modules/ipahbacsvc.py @@ -180,19 +180,9 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - ansible_module.ipa_command(command, name, args) - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From ac8f3b047c36a5b46b06582d457f645c7d680710 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:54:08 +0200 Subject: [PATCH 08/22] hostgroup: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipahostgroup.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/plugins/modules/ipahostgroup.py b/plugins/modules/ipahostgroup.py index b0f7857b..b2f553f7 100644 --- a/plugins/modules/ipahostgroup.py +++ b/plugins/modules/ipahostgroup.py @@ -490,34 +490,10 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - errors = [] - for failed_item in result.get("failed", []): - failed = result["failed"][failed_item] - for member_type in failed: - for member, failure in failed[member_type]: - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + + changed = ansible_module.execute_ipa_commands( + commands, fail_on_member_errors=True) # Done From 4392e32f6ec55a4eeb487197ff447408423596be Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:54:34 +0200 Subject: [PATCH 09/22] location: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipalocation.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/plugins/modules/ipalocation.py b/plugins/modules/ipalocation.py index 48d5b487..7f10b944 100644 --- a/plugins/modules/ipalocation.py +++ b/plugins/modules/ipalocation.py @@ -168,23 +168,8 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From ffdae9cee87cef3db4ee28aed5791f90e2cc30bf Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:55:20 +0200 Subject: [PATCH 10/22] permission: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipapermission.py | 47 ++++++++++++-------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/plugins/modules/ipapermission.py b/plugins/modules/ipapermission.py index 5921675f..2dc6ab1c 100644 --- a/plugins/modules/ipapermission.py +++ b/plugins/modules/ipapermission.py @@ -180,6 +180,22 @@ def gen_args(right, attrs, bindtype, subtree, return _args +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors): + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + for failed_item in result.get("failed", []): + failed = result["failed"][failed_item] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + def main(): ansible_module = IPAAnsibleModule( argument_spec=dict( @@ -425,38 +441,9 @@ def main(): else: ansible_module.fail_json(msg="Unknown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - errors = [] - for failed_item in result.get("failed", []): - failed = result["failed"][failed_item] - for member_type in failed: - for member, failure in failed[member_type]: - if "already a member" in failure \ - or "not a member" in failure: - continue - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands(commands, result_handler) # Done From 6c60b738a5c627eae0f124e65f33f5e436a82ad9 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:56:36 +0200 Subject: [PATCH 11/22] privilege: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipaprivilege.py | 47 ++++++++++++--------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/plugins/modules/ipaprivilege.py b/plugins/modules/ipaprivilege.py index 3256eba9..7b32468f 100644 --- a/plugins/modules/ipaprivilege.py +++ b/plugins/modules/ipaprivilege.py @@ -126,6 +126,22 @@ def find_privilege(module, name): return _result["result"] +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors): + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + for failed_item in result.get("failed", []): + failed = result["failed"][failed_item] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + def main(): ansible_module = IPAAnsibleModule( argument_spec=dict( @@ -304,38 +320,9 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json( - msg="%s: %s: %s" % (command, name, str(e))) - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - errors = [] - for failed_item in result.get("failed", []): - failed = result["failed"][failed_item] - for member_type in failed: - for member, failure in failed[member_type]: - if "already a member" in failure \ - or "not a member" in failure: - continue - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands(commands, result_handler) # Done From a11c442902ebdfd6dc3feb9d0541d3354e2b34b9 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:56:56 +0200 Subject: [PATCH 12/22] pwpolicy: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipapwpolicy.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/plugins/modules/ipapwpolicy.py b/plugins/modules/ipapwpolicy.py index 758ee6db..55bedd07 100644 --- a/plugins/modules/ipapwpolicy.py +++ b/plugins/modules/ipapwpolicy.py @@ -272,19 +272,9 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - ansible_module.ipa_command(command, name, args) - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From 01ef557ba6bbbcb3814cc461c31185b1ff67d139 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:57:24 +0200 Subject: [PATCH 13/22] role: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/iparole.py | 41 +++++++------------------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/plugins/modules/iparole.py b/plugins/modules/iparole.py index 4a073e6d..55b1e1e3 100644 --- a/plugins/modules/iparole.py +++ b/plugins/modules/iparole.py @@ -314,12 +314,12 @@ def ensure_members_are_present(module, name, res_find): return commands -def process_command_failures(command, result): +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors): """Process the result of a command, looking for errors.""" # Get all errors # All "already a member" and "not a member" failures in the # result are ignored. All others are reported. - errors = [] if "failed" in result and len(result["failed"]) > 0: for item in result["failed"]: failed_item = result["failed"][item] @@ -330,37 +330,6 @@ def process_command_failures(command, result): continue errors.append("%s: %s %s: %s" % ( command, member_type, member, failure)) - return errors - - -def process_commands(module, commands): - """Process the list of IPA API commands.""" - errors = [] - exit_args = {} - changed = False - - # Check mode exit - if module.check_mode: - return len(commands) > 0, exit_args - - for name, command, args in commands: - try: - result = module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - - errors = process_command_failures(command, result) - except Exception as exception: # pylint: disable=broad-except - module.fail_json( - msg="%s: %s: %s" % (command, name, str(exception))) - - if errors: - module.fail_json(msg=", ".join(errors)) - - return changed, exit_args def role_commands_for_name(module, state, action, name): @@ -454,7 +423,11 @@ def main(): cmds = role_commands_for_name(ansible_module, state, action, name) commands.extend(cmds) - changed, exit_args = process_commands(ansible_module, commands) + exit_args = {} + + # Execute commands + + changed = ansible_module.execute_ipa_commands(commands, result_handler) # Done ansible_module.exit_json(changed=changed, **exit_args) From fba1cc1440fa41de77da986fdc85d135af46b5cd Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:57:44 +0200 Subject: [PATCH 14/22] selfservice: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipaselfservice.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/plugins/modules/ipaselfservice.py b/plugins/modules/ipaselfservice.py index 81b4461c..53bd5b3b 100644 --- a/plugins/modules/ipaselfservice.py +++ b/plugins/modules/ipaselfservice.py @@ -278,17 +278,7 @@ def main(): # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From 06e5382320279103b26acafa3072f2d214ae025a Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:58:23 +0200 Subject: [PATCH 15/22] server: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipaserver.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/plugins/modules/ipaserver.py b/plugins/modules/ipaserver.py index c1bfe119..509e3ca1 100644 --- a/plugins/modules/ipaserver.py +++ b/plugins/modules/ipaserver.py @@ -391,17 +391,7 @@ def main(): # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From dc1027c3cae09f1a1c73e0bdc02b1b20921f392c Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:58:57 +0200 Subject: [PATCH 16/22] service: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipaservice.py | 45 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/plugins/modules/ipaservice.py b/plugins/modules/ipaservice.py index b72e0915..29ef992b 100644 --- a/plugins/modules/ipaservice.py +++ b/plugins/modules/ipaservice.py @@ -409,6 +409,23 @@ def init_ansible_module(): return ansible_module +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors): + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + def main(): ansible_module = init_ansible_module() @@ -831,34 +848,8 @@ def main(): ansible_module.exit_json(changed=len(commands) > 0, **exit_args) # Execute commands - errors = [] - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as ex: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(ex))) - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - if "failed" in result and len(result["failed"]) > 0: - for item in result["failed"]: - failed_item = result["failed"][item] - for member_type in failed_item: - for member, failure in failed_item[member_type]: - if "already a member" in failure \ - or "not a member" in failure: - continue - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands(commands, result_handler) # Done ansible_module.exit_json(changed=changed, **exit_args) From b5bfcc13f9d3fc5ab3aa3367fbea70a07cb53667 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 3 Sep 2021 18:59:54 +0200 Subject: [PATCH 17/22] sudorule: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipasudorule.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/plugins/modules/ipasudorule.py b/plugins/modules/ipasudorule.py index ca60d090..a149c75c 100644 --- a/plugins/modules/ipasudorule.py +++ b/plugins/modules/ipasudorule.py @@ -824,36 +824,10 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - errors = [] - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as ex: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(ex))) - # Get all errors - # result are ignored. All others are reported. - if "failed" in result and len(result["failed"]) > 0: - for item in result["failed"]: - failed_item = result["failed"][item] - for member_type in failed_item: - for member, failure in failed_item[member_type]: - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands( + commands, fail_on_member_errors=True) # Done From 5c38d43ce37073474298ad07b0a0900c8f49c938 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Tue, 7 Sep 2021 11:20:42 +0200 Subject: [PATCH 18/22] utils/templates/ipamodul.py.ine: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- utils/templates/ipamodule.py.in | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/utils/templates/ipamodule.py.in b/utils/templates/ipamodule.py.in index f7d1e538..b179a0b8 100644 --- a/utils/templates/ipamodule.py.in +++ b/utils/templates/ipamodule.py.in @@ -194,23 +194,9 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) + changed = ansible_module.execute_ipa_commands(commands) # Done From e897ecb27a7aeaac9cf90c06e2920516a18cbaee Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Tue, 7 Sep 2021 11:21:32 +0200 Subject: [PATCH 19/22] utils/templates/ipamodule+member.py.in: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- utils/templates/ipamodule+member.py.in | 54 ++++++++++++-------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/utils/templates/ipamodule+member.py.in b/utils/templates/ipamodule+member.py.in index c3e37727..7e7ad8ea 100644 --- a/utils/templates/ipamodule+member.py.in +++ b/utils/templates/ipamodule+member.py.in @@ -286,38 +286,34 @@ def main(): else: ansible_module.fail_json(msg="Unkown state '%s'" % state) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except Exception as e: - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - str(e))) - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - errors = [] - for failed_item in result.get("failed", []): - failed = result["failed"][failed_item] - for member_type in failed: - for member, failure in failed[member_type]: - if "already a member" in failure \ - or "not a member" in failure: - continue - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + # + # To handle default member errors there is a static method + # IPAAnsibleModule.handle_member_errors. It can be enabled with + # fail_on_member_failures=True for execute_ipa_commands. + # There might be cases in which this needs to be either done + # manually or extended. + # + # Example: + # + # pylint: disable=unused-argument + # def result_handler(module, result, command, name, args, errors): + # # Get all errors + # IPAAnsibleModule.handle_member_errors(module, result, command, + # name, args, errors) + # if "MY ERROR" in result.get("failed",[]): + # errors.append("My error") + # + # # Execute commands + # + # changed = ansible_module.execute_ipa_commands(commands, + # result_handler) + # + + changed = ansible_module.execute_ipa_commands( + commands, fail_on_member_failures=True) # Done From 3d13e7b6c0094bb444db3c60914bf2a3d4200ad8 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Tue, 7 Sep 2021 16:14:29 +0200 Subject: [PATCH 20/22] user: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipauser.py | 93 ++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index 7e44a3f3..1ffee448 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -716,6 +716,46 @@ def gen_certmapdata_args(certmapdata): return {"ipacertmapdata": to_text(certmapdata)} +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors, exit_args, + one_name): + + if "random" in args and command in ["user_add", "user_mod"] \ + and "randompassword" in result["result"]: + if one_name: + exit_args["randompassword"] = \ + result["result"]["randompassword"] + else: + exit_args.setdefault(name, {})["randompassword"] = \ + result["result"]["randompassword"] + + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + +# pylint: disable=unused-argument +def exception_handler(module, ex, errors, exit_args, one_name): + msg = str(ex) + if "already contains" in msg \ + or "does not contain" in msg: + return True + # The canonical principal name may not be removed + if "equal to the canonical principal name must" in msg: + return True + return False + + def main(): user_spec = dict( # present @@ -1359,58 +1399,11 @@ def main(): del user_set - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - errors = [] - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - - if "random" in args and command in ["user_add", "user_mod"] \ - and "randompassword" in result["result"]: - if len(names) == 1: - exit_args["randompassword"] = \ - result["result"]["randompassword"] - else: - exit_args.setdefault(name, {})["randompassword"] = \ - result["result"]["randompassword"] - - except Exception as e: - msg = str(e) - if "already contains" in msg \ - or "does not contain" in msg: - continue - # The canonical principal name may not be removed - if "equal to the canonical principal name must" in msg: - continue - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - msg)) - - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - if "failed" in result and len(result["failed"]) > 0: - for item in result["failed"]: - failed_item = result["failed"][item] - for member_type in failed_item: - for member, failure in failed_item[member_type]: - if "already a member" in failure \ - or "not a member" in failure: - continue - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands( + commands, result_handler, exception_handler, + exit_args=exit_args, one_name=len(names) == 1) # Done ansible_module.exit_json(changed=changed, user=exit_args) From a4aee3b2a65de2dd96a9e119e8d2b7ec246080cc Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Tue, 7 Sep 2021 16:15:55 +0200 Subject: [PATCH 21/22] host: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipahost.py | 111 +++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 60 deletions(-) diff --git a/plugins/modules/ipahost.py b/plugins/modules/ipahost.py index 86453a77..93403001 100644 --- a/plugins/modules/ipahost.py +++ b/plugins/modules/ipahost.py @@ -572,6 +572,54 @@ def check_parameters( # pylint: disable=unused-argument "'member' for state '%s'" % (x, state)) +# pylint: disable=unused-argument +def result_handler(module, result, command, name, args, errors, exit_args, + one_name): + if "random" in args and command in ["host_add", "host_mod"] \ + and "randompassword" in result["result"]: + if one_name: + exit_args["randompassword"] = \ + result["result"]["randompassword"] + else: + exit_args.setdefault(name, {})["randompassword"] = \ + result["result"]["randompassword"] + + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + +# pylint: disable=unused-argument +def exception_handler(module, ex, errors, exit_args, one_name): + msg = str(ex) + if "already contains" in msg \ + or "does not contain" in msg: + return True + + # The canonical principal name may not be removed + if "equal to the canonical principal name must" in msg: + return True + + # Host is already disabled, ignore error + if "This entry is already disabled" in msg: + return True + + # Ignore no modification error. + if "no modifications to be performed" in msg: + return True + + return False + + def main(): host_spec = dict( # present @@ -1343,68 +1391,11 @@ def main(): del host_set - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - errors = [] - for name, command, args in commands: - try: - result = ansible_module.ipa_command(command, name, args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - - if "random" in args and command in ["host_add", "host_mod"] \ - and "randompassword" in result["result"]: - if len(names) == 1: - exit_args["randompassword"] = \ - result["result"]["randompassword"] - else: - exit_args.setdefault(name, {})["randompassword"] = \ - result["result"]["randompassword"] - - except Exception as e: - msg = str(e) - if "already contains" in msg \ - or "does not contain" in msg: - continue - - # The canonical principal name may not be removed - if "equal to the canonical principal name must" in msg: - continue - - # Host is already disabled, ignore error - if "This entry is already disabled" in msg: - continue - - # Ignore no modification error. - if "no modifications to be performed" in msg: - continue - - ansible_module.fail_json(msg="%s: %s: %s" % (command, name, - msg)) - - # Get all errors - # All "already a member" and "not a member" failures in the - # result are ignored. All others are reported. - if "failed" in result and len(result["failed"]) > 0: - for item in result["failed"]: - failed_item = result["failed"][item] - for member_type in failed_item: - for member, failure in failed_item[member_type]: - if "already a member" in failure \ - or "not a member" in failure: - continue - errors.append("%s: %s %s: %s" % ( - command, member_type, member, failure)) - - if len(errors) > 0: - ansible_module.fail_json(msg=", ".join(errors)) + changed = ansible_module.execute_ipa_commands( + commands, result_handler, exception_handler, + exit_args=exit_args, one_name=len(names) == 1) # Done From 15d7cbbf2b40319eb217a54a34eac84a1f05a372 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Tue, 7 Sep 2021 21:17:29 +0200 Subject: [PATCH 22/22] dnsrecord: Use execute_ipa_commands execute_ipa_commands replces the check mode exit, the loop over the generated commands and also in the member failure handling for modules with member support. --- plugins/modules/ipadnsrecord.py | 34 +++++++++++---------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/plugins/modules/ipadnsrecord.py b/plugins/modules/ipadnsrecord.py index 3950b313..69b9212a 100644 --- a/plugins/modules/ipadnsrecord.py +++ b/plugins/modules/ipadnsrecord.py @@ -1241,7 +1241,7 @@ def create_reverse_ip_record(module, zone_name, name, ips): 'idnsname': to_text(reverse_host), "ptrrecord": "%s.%s" % (name, zone_name) } - _cmds.append([reverse_zone, 'dnsrecord_add', rev_args]) + _cmds.append([to_text(reverse_zone), 'dnsrecord_add', rev_args]) return _cmds @@ -1408,6 +1408,14 @@ def define_commands_for_absent_state(module, zone_name, entry, res_find): return _commands +# pylint: disable=unused-argument +def exception_handler(module, ex): + if isinstance(ex, (ipalib_errors.EmptyModlist, + ipalib_errors.DuplicateEntry)): + return True + return False + + def main(): """Execute DNS record playbook.""" ansible_module = configure_module() @@ -1468,30 +1476,10 @@ def main(): if cmds: commands.extend(cmds) - # Check mode exit - if ansible_module.check_mode: - ansible_module.exit_json(changed=len(commands) > 0, **exit_args) - # Execute commands - for name, command, args in commands: - try: - result = ansible_module.ipa_command( - command, to_text(name), args) - if "completed" in result: - if result["completed"] > 0: - changed = True - else: - changed = True - except ipalib_errors.EmptyModlist: - continue - except ipalib_errors.DuplicateEntry: - continue - except Exception as e: - error_message = str(e) - - ansible_module.fail_json( - msg="%s: %s: %s" % (command, name, error_message)) + changed = ansible_module.execute_ipa_commands( + commands, exception_handler=exception_handler) # Done ansible_module.exit_json(changed=changed, host=exit_args)