diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py index 1cc02198..aaac4882 100644 --- a/ansible_mitogen/runner.py +++ b/ansible_mitogen/runner.py @@ -627,6 +627,14 @@ class NewStyleRunner(ScriptRunner): for fullname in self.module_map['builtin']: mitogen.core.import_module(fullname) + def _setup_excepthook(self): + """ + Starting with Ansible 2.6, some modules (file.py) install a + sys.excepthook and never clean it up. So we must preserve the original + excepthook and restore it after the run completes. + """ + self.original_excepthook = sys.excepthook + def setup(self): super(NewStyleRunner, self).setup() @@ -640,12 +648,17 @@ class NewStyleRunner(ScriptRunner): module_utils=self.module_map['custom'], ) self._setup_imports() + self._setup_excepthook() if libc__res_init: libc__res_init() + def _revert_excepthook(self): + sys.excepthook = self.original_excepthook + def revert(self): self._argv.revert() self._stdio.revert() + self._revert_excepthook() super(NewStyleRunner, self).revert() def _get_program_filename(self): @@ -679,6 +692,20 @@ class NewStyleRunner(ScriptRunner): else: main_module_name = b'__main__' + def _handle_magic_exception(self, mod, exc): + """ + Beginning with Ansible >2.6, some modules (file.py) install a + sys.excepthook which is a closure over AnsibleModule, redirecting the + magical exception to AnsibleModule.fail_json(). + + For extra special needs bonus points, the class is not defined in + module_utils, but is defined in the module itself, meaning there is no + type for isinstance() that outlasts the invocation. + """ + klass = getattr(mod, 'AnsibleModuleError', None) + if klass and isinstance(exc, klass): + mod.module.fail_json(**exc.results) + def _run(self): code = self._get_code() @@ -695,10 +722,14 @@ class NewStyleRunner(ScriptRunner): exc = None try: - if mitogen.core.PY3: - exec(code, vars(mod)) - else: - exec('exec code in vars(mod)') + try: + if mitogen.core.PY3: + exec(code, vars(mod)) + else: + exec('exec code in vars(mod)') + except Exception as e: + self._handle_magic_exception(mod, e) + raise except SystemExit as e: exc = e diff --git a/tests/ansible/regression/all.yml b/tests/ansible/regression/all.yml index ecb9638c..46798b3e 100644 --- a/tests/ansible/regression/all.yml +++ b/tests/ansible/regression/all.yml @@ -7,3 +7,4 @@ - import_playbook: issue_152__virtualenv_python_fails.yml - import_playbook: issue_154__module_state_leaks.yml - import_playbook: issue_177__copy_module_failing.yml +- import_playbook: issue_332_ansiblemoduleerror_first_occurrence.yml diff --git a/tests/ansible/regression/issue_332_ansiblemoduleerror_first_occurrence.yml b/tests/ansible/regression/issue_332_ansiblemoduleerror_first_occurrence.yml new file mode 100644 index 00000000..3a64455e --- /dev/null +++ b/tests/ansible/regression/issue_332_ansiblemoduleerror_first_occurrence.yml @@ -0,0 +1,14 @@ +# issue #332: Ansible 2.6 file.py started defining an excepthook and private +# AnsibleModuleError. Ensure file fails correctly. + +- name: regression/issue_332_ansiblemoduleerror_first_occurrence.yml + hosts: all + tasks: + - file: path=/usr/bin/does-not-exist mode='a-s' state=file follow=yes + ignore_errors: true + register: out + + - assert: + that: + - out.state == 'absent' + - out.msg == 'file (/usr/bin/does-not-exist) is absent, cannot continue'