diff --git a/ansible_mitogen/executor.py b/ansible_mitogen/runner.py similarity index 71% rename from ansible_mitogen/executor.py rename to ansible_mitogen/runner.py index 00e91d05..559001f0 100644 --- a/ansible_mitogen/executor.py +++ b/ansible_mitogen/runner.py @@ -31,7 +31,7 @@ import json import os import tempfile -import ansible_mitogen.helpers +import ansible_mitogen.helpers # TODO: circular import try: from shlex import quote as shlex_quote @@ -43,23 +43,6 @@ import ansible.module_utils.basic ansible.module_utils.basic._ANSIBLE_ARGS = '{}' -class Exit(Exception): - """ - Raised when a module exits with success. - """ - def __init__(self, dct): - self.dct = dct - - -class ModuleError(Exception): - """ - Raised when a module voluntarily indicates failure via .fail_json(). - """ - def __init__(self, msg, dct): - Exception.__init__(self, msg) - self.dct = dct - - class TemporaryEnvironment(object): def __init__(self, env=None): self.original = os.environ.copy() @@ -71,44 +54,41 @@ class TemporaryEnvironment(object): os.environ.update(self.original) -class MethodOverrides(object): - @staticmethod - def exit_json(self, **kwargs): - """ - Replace AnsibleModule.exit_json() with something that doesn't try to - kill the process or JSON-encode the result dictionary. Instead, cause - Exit to be raised, with a `dct` attribute containing the successful - result dictionary. - """ - self.add_path_info(kwargs) - kwargs.setdefault('changed', False) +class NativeModuleExit(Exception): + """ + Capture the result of a call to `.exit_json()` or `.fail_json()` by a + native Ansible module. + """ + def __init__(self, ansible_module, **kwargs): + ansible_module.add_path_info(kwargs) kwargs.setdefault('invocation', { - 'module_args': self.params + 'module_args': ansible_module.params }) - kwargs = ansible.module_utils.basic.remove_values( + ansible_module.do_cleanup_files() + self.dct = ansible.module_utils.basic.remove_values( kwargs, self.no_log_values, ) - self.do_cleanup_files() - raise Exit(kwargs) + + +class NativeMethodOverrides(object): + @staticmethod + def exit_json(self, **kwargs): + """ + Raise exit_json() output as the `.dct` attribute of a + :class:`NativeModuleExit` exception`. + """ + kwargs.setdefault('changed', False) + return NativeModuleExit(self, **kwargs) @staticmethod def fail_json(self, **kwargs): """ - Replace AnsibleModule.fail_json() with something that raises - ModuleError, which includes a `dct` attribute. + Raise fail_json() output as the `.dct` attribute of a + :class:`NativeModuleExit` exception`. """ - self.add_path_info(kwargs) kwargs.setdefault('failed', True) - kwargs.setdefault('invocation', { - 'module_args': self.params - }) - kwargs = ansible.module_utils.basic.remove_values( - kwargs, - self.no_log_values, - ) - self.do_cleanup_files() - raise ModuleError(kwargs.get('msg'), kwargs) + return NativeModuleExit(self, **kwargs) klass = ansible.module_utils.basic.AnsibleModule @@ -119,14 +99,16 @@ class MethodOverrides(object): self.klass.fail_json = self.fail_json def revert(self): + """ + Restore prior state. + """ self.klass.exit_json = self._original_exit_json self.klass.fail_json = self._original_fail_json -class ModuleArguments(object): +class NativeModuleArguments(object): """ - Patch the ansible.module_utils.basic global arguments variable on - construction, and revert the changes on call to :meth:`revert`. + Patch ansible.module_utils.basic argument globals. """ def __init__(self, args): self.original = ansible.module_utils.basic._ANSIBLE_ARGS @@ -135,29 +117,44 @@ class ModuleArguments(object): }) def revert(self): + """ + Restore prior state. + """ ansible.module_utils.basic._ANSIBLE_ARGS = self._original_args class Runner(object): - def __init__(self, module, raw_params=None, args=None, env=None, - runner_params=None): + """ + Ansible module runner. After instantiation (with kwargs supplied by the + corresponding Planner), `.run()` is invoked, upon which `setup()`, + `_run()`, and `revert()` are invoked, with the return value of `_run()` + returned by `run()`. + + Subclasses may override `_run`()` and extend `setup()` and `revert()`. + """ + def __init__(self, module, raw_params=None, args=None, env=None): if args is None: args = {} if raw_params is not None: args['_raw_params'] = raw_params - if runner_params is None: - runner_params = {} self.module = module self.raw_params = raw_params self.args = args self.env = env - self.runner_params = runner_params def setup(self): + """ + Prepare the current process for running a module. The base + implementation simply prepares the environment. + """ self._env = TemporaryEnvironment(self.env) def revert(self): + """ + Revert any changes made to the process after running a module. The base + implementation simply restores the original environment. + """ self._env.revert() def _run(self): @@ -169,6 +166,9 @@ class Runner(object): module. This monkey-patches the Ansible libraries in various places to prevent it from trying to kill the process on completion, and to prevent it from reading sys.stdin. + + :returns: + Module result dictionary. """ self.setup() try: @@ -177,37 +177,53 @@ class Runner(object): self.revert() -class PythonRunner(object): +class NativeRunner(object): """ Execute a new-style Ansible module, where Module Replacer-related tricks aren't required. """ def setup(self): - super(PythonRunner, self).setup() - self._overrides = MethodOverrides() - self._args = ModuleArguments(self.args) + super(NativeRunner, self).setup() + self._overrides = NativeMethodOverrides() + self._args = NativeModuleArguments(self.args) def revert(self): - super(PythonRunner, self).revert() + super(NativeRunner, self).revert() self._args.revert() self._overrides.revert() + def module_fixups(mod): + """ + Apply fixups for known problems with mainline Ansible modules. + """ + if mod.__name__ == 'ansible.modules.packaging.os.yum_repository': + # https://github.com/dw/mitogen/issues/154 + mod.YumRepo.repofile = mod.configparser.RawConfigParser() + def _run(self): try: mod = __import__(self.module, {}, {}, ['']) + self.module_fixups(mod) # Ansible modules begin execution on import. Thus the above # __import__ will cause either Exit or ModuleError to be raised. If # we reach the line below, the module did not execute and must # already have been imported for a previous invocation, so we need # to invoke main explicitly. mod.main() - except (Exit, ModuleError), e: - return json.dumps(e.dct) + except NativeModuleExit, e: + return e.dct - assert False, "Module returned no result." + return { + 'failed': True, + 'msg': 'ansible_mitogen: module did not exit normally.' + } class BinaryRunner(object): + def __init__(self, path, **kwargs): + super(BinaryRunner, self).__init__(**kwargs) + self.path = path + def setup(self): super(BinaryRunner, self).setup() self._setup_binary() @@ -261,9 +277,12 @@ class BinaryRunner(object): super(BinaryRunner, self).revert() def _run(self): - rc, stdout, stderr = ansible_mitogen.helpers.exec_args( - args=[self.bin_fp.name, self.args_fp.name], - ) + try: + rc, stdout, stderr = ansible_mitogen.helpers.exec_args( + args=[self.bin_fp.name, self.args_fp.name], + ) + except Exception, e: + return # ... assert 0