#!/usr/bin/env python # (c) 2012, Michael DeHaan # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # # this script is for testing modules without running through the # entire guts of ansible, and is very helpful for when developing # modules # # example: # test-module -m ../library/commands/command -a "/bin/sleep 3" # test-module -m ../library/system/service -a "name=httpd ensure=restarted" # test-module -m ../library/system/service -a "name=httpd ensure=restarted" --debugger /usr/bin/pdb # test-module -m ../library/file/lineinfile -a "dest=/etc/exports line='/srv/home hostname1(rw,sync)'" --check # test-module -m ../library/commands/command -a "echo hello" -n -o "test_hello" import base64 from multiprocessing import Lock import optparse import os import subprocess import sys import traceback import ansible.utils.vars as utils_vars from ansible.parsing.dataloader import DataLoader from ansible.parsing.utils.jsonify import jsonify from ansible.parsing.splitter import parse_kv import ansible.executor.module_common as module_common import ansible.constants as C try: import json except ImportError: import simplejson as json def parse(): """parse command line :return : (options, args)""" parser = optparse.OptionParser() parser.usage = "%prog -[options] (-h for help)" parser.add_option('-m', '--module-path', dest='module_path', help="REQUIRED: full path of module source to execute") parser.add_option('-a', '--args', dest='module_args', default="", help="module argument string") parser.add_option('-D', '--debugger', dest='debugger', help="path to python debugger (e.g. /usr/bin/pdb)") parser.add_option('-I', '--interpreter', dest='interpreter', help="path to interpreter to use for this module (e.g. ansible_python_interpreter=/usr/bin/python)", metavar='INTERPRETER_TYPE=INTERPRETER_PATH', default='python={0}'.format(sys.executable)) parser.add_option('-c', '--check', dest='check', action='store_true', help="run the module in check mode") parser.add_option('-n', '--noexecute', dest='execute', action='store_false', default=True, help="do not run the resulting module") parser.add_option('-o', '--output', dest='filename', help="Filename for resulting module", default="~/.ansible_module_generated") options, args = parser.parse_args() if not options.module_path: parser.print_help() sys.exit(1) else: return options, args def write_argsfile(argstring, json=False): """ Write args to a file for old-style module's use. """ argspath = os.path.expanduser("~/.ansible_test_module_arguments") argsfile = open(argspath, 'w') if json: args = parse_kv(argstring) argstring = jsonify(args) argsfile.write(argstring) argsfile.close() return argspath def boilerplate_module(modfile, args, interpreter, check, destfile): """ simulate what ansible does with new style modules """ #module_fh = open(modfile) #module_data = module_fh.read() #module_fh.close() #replacer = module_common.ModuleReplacer() loader = DataLoader() #included_boilerplate = module_data.find(module_common.REPLACER) != -1 or module_data.find("import ansible.module_utils") != -1 complex_args = {} if args.startswith("@"): # Argument is a YAML file (JSON is a subset of YAML) complex_args = utils_vars.combine_vars(complex_args, loader.load_from_file(args[1:])) args='' elif args.startswith("{"): # Argument is a YAML document (not a file) complex_args = utils_vars.combine_vars(complex_args, loader.load(args)) args='' if args: parsed_args = parse_kv(args) complex_args = utils_vars.combine_vars(complex_args, parsed_args) task_vars = {} if interpreter: if '=' not in interpreter: print("interpreter must by in the form of ansible_python_interpreter=/usr/bin/python") sys.exit(1) interpreter_type, interpreter_path = interpreter.split('=') if not interpreter_type.startswith('ansible_'): interpreter_type = 'ansible_%s' % interpreter_type if not interpreter_type.endswith('_interpreter'): interpreter_type = '%s_interpreter' % interpreter_type task_vars[interpreter_type] = interpreter_path if check: complex_args['_ansible_check_mode'] = True modname = os.path.basename(modfile) modname = os.path.splitext(modname)[0] action_write_lock = Lock() (module_data, module_style, shebang) = module_common.modify_module( modname, modfile, complex_args, action_write_lock, task_vars=task_vars ) modfile2_path = os.path.expanduser(destfile) print("* including generated source, if any, saving to: %s" % modfile2_path) print("* this may offset any line numbers in tracebacks/debuggers!") modfile2 = open(modfile2_path, 'w') modfile2.write(module_data) modfile2.close() modfile = modfile2_path return (modfile2_path, module_style) def runtest( modfile, argspath): """Test run a module, piping it's output for reporting.""" os.system("chmod +x %s" % modfile) invoke = "%s" % (modfile) if argspath is not None: invoke = "%s %s" % (modfile, argspath) cmd = subprocess.Popen(invoke, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = cmd.communicate() try: print("***********************************") print("RAW OUTPUT") print(out) print(err) results = json.loads(out) except: print("***********************************") print("INVALID OUTPUT FORMAT") print(out) traceback.print_exc() sys.exit(1) print("***********************************") print("PARSED OUTPUT") print(jsonify(results,format=True)) def rundebug(debugger, modfile, argspath): """Run interactively with console debugger.""" if argspath is not None: subprocess.call("%s %s %s" % (debugger, modfile, argspath), shell=True) else: subprocess.call("%s %s" % (debugger, modfile), shell=True) def main(): options, args = parse() (modfile, module_style) = boilerplate_module(options.module_path, options.module_args, options.interpreter, options.check, options.filename) argspath = None if module_style != 'new': if module_style == 'non_native_want_json': argspath = write_argsfile(options.module_args, json=True) elif module_style == 'old': argspath = write_argsfile(options.module_args, json=False) else: raise Exception("internal error, unexpected module style: %s" % module_style) if options.execute: if options.debugger: rundebug(options.debugger, modfile, argspath) else: runtest(modfile, argspath) if __name__ == "__main__": main()