# Copyright 2017, David Wilson # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its contributors # may be used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ Ansible is so poorly layered that attempting to import anything under ansible.plugins automatically triggers import of __main__, which causes remote execution of the ansible command-line tool. :( So here we define helpers in some sanely layered package where the entirety of Ansible won't be imported. """ import json import subprocess import time # Prevent accidental import of an Ansible module from hanging on stdin read. 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 def wtf_exit_json(self, **kwargs): """ Replace AnsibleModule.exit_json() with something that doesn't try to suicide the process or JSON-encode the 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) kwargs.setdefault('invocation', { 'module_args': self.params }) kwargs = ansible.module_utils.basic.remove_values(kwargs, self.no_log_values) self.do_cleanup_files() raise Exit(kwargs) def wtf_fail_json(self, **kwargs): """ Replace AnsibleModule.fail_json() with something that raises ModuleError, which includes a `dct` attribute. """ 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) def run_module(module, raw_params=None, args=None): """ Set up the process environment in preparation for running an Ansible module. The 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. """ if args is None: args = {} if raw_params is not None: args['_raw_params'] = raw_params ansible.module_utils.basic.AnsibleModule.exit_json = wtf_exit_json ansible.module_utils.basic.AnsibleModule.fail_json = wtf_fail_json ansible.module_utils.basic._ANSIBLE_ARGS = json.dumps({ 'ANSIBLE_MODULE_ARGS': args }) try: mod = __import__(module, {}, {}, ['']) # Ansible modules begin execution on import, because they're crap from # hell. 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, e: return json.dumps(e.dct) def exec_command(cmd, in_data=None): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) stdout, stderr = proc.communicate(in_data) return proc.returncode, stdout, stderr def read_path(path): return open(path, 'rb').read() def write_path(path, s): open(path, 'wb').write(s)