hacking/test-module
ee274b58
 #!/usr/bin/env python
a735dd2b
 
 # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
 #
 # 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 <http://www.gnu.org/licenses/>.
 #
 
 # this script is for testing modules without running through the
 # entire guts of ansible, and is very helpful for when developing
 # modules
 #
 # example:
b8cbf137
 #    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
8342cc6b
 #    test-module -m ../library/file/lineinfile -a "dest=/etc/exports line='/srv/home hostname1(rw,sync)'" --check
b05485d4
 #    test-module -m ../library/commands/command -a "echo hello" -n -o "test_hello"
a735dd2b
 
dcc5dfdf
 import optparse
a735dd2b
 import os
 import subprocess
dcc5dfdf
 import sys
a735dd2b
 import traceback
bdd73e31
 import shutil
dcc5dfdf
 
2f51f3bb
 import ansible.utils.vars as utils_vars
4203850d
 from ansible.parsing.dataloader import DataLoader
ea6ec3bf
 from ansible.parsing.utils.jsonify import jsonify
5466ff89
 from ansible.parsing.splitter import parse_kv
3c7a502c
 import ansible.executor.module_common as module_common
06e54c0b
 import ansible.constants as C
a735dd2b
 
 try:
     import json
 except ImportError:
     import simplejson as json
 
08208285
 
ade0233d
 def parse():
     """parse command line
 
     :return : (options, args)"""
     parser = optparse.OptionParser()
 
738cea9c
     parser.usage = "%prog -[options] (-h for help)"
ade0233d
 
e8583833
     parser.add_option('-m', '--module-path', dest='module_path',
738cea9c
         help="REQUIRED: full path of module source to execute")
ade0233d
     parser.add_option('-a', '--args', dest='module_args', default="",
738cea9c
         help="module argument string")
08208285
     parser.add_option('-D', '--debugger', dest='debugger',
ade0233d
         help="path to python debugger (e.g. /usr/bin/pdb)")
e50c2bcc
     parser.add_option('-I', '--interpreter', dest='interpreter',
bf5d8ee6
         help="path to interpreter to use for this module (e.g. ansible_python_interpreter=/usr/bin/python)",
08208285
         metavar='INTERPRETER_TYPE=INTERPRETER_PATH')
b8cbf137
     parser.add_option('-c', '--check', dest='check', action='store_true',
         help="run the module in check mode")
b05485d4
     parser.add_option('-n', '--noexecute', dest='execute', action='store_false',
         default=True, help="do not run the resulting module")
8342cc6b
     parser.add_option('-o', '--output', dest='filename',
b05485d4
         help="Filename for resulting module",
         default="~/.ansible_module_generated")
ade0233d
     options, args = parser.parse_args()
e8583833
     if not options.module_path:
ade0233d
         parser.print_help()
         sys.exit(1)
     else:
         return options, args
 
08208285
 
af2fb56a
 def write_argsfile(argstring, json=False):
fc96b882
     """ Write args to a file for old-style module's use. """
ade0233d
     argspath = os.path.expanduser("~/.ansible_test_module_arguments")
     argsfile = open(argspath, 'w')
af2fb56a
     if json:
5466ff89
         args = parse_kv(argstring)
ea6ec3bf
         argstring = jsonify(args)
ade0233d
     argsfile.write(argstring)
     argsfile.close()
     return argspath
 
08208285
 
 def get_interpreters(interpreter):
     result = dict()
     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
         result[interpreter_type] = interpreter_path
     return result
 
 
 def boilerplate_module(modfile, args, interpreters, check, destfile):
fc96b882
     """ simulate what ansible does with new style modules """
 
08208285
     # module_fh = open(modfile)
     # module_data = module_fh.read()
     # module_fh.close()
d34a26e3
 
08208285
     # replacer = module_common.ModuleReplacer()
2f51f3bb
     loader = DataLoader()
d34a26e3
 
08208285
     # included_boilerplate = module_data.find(module_common.REPLACER) != -1 or module_data.find("import ansible.module_utils") != -1
d34a26e3
 
     complex_args = {}
cf39a1ab
 
     # default selinux fs list is pass in as _ansible_selinux_special_fs arg
     complex_args['_ansible_selinux_special_fs'] = C.DEFAULT_SELINUX_SPECIAL_FS
 
dbed05ca
     if args.startswith("@"):
         # Argument is a YAML file (JSON is a subset of YAML)
2f51f3bb
         complex_args = utils_vars.combine_vars(complex_args, loader.load_from_file(args[1:]))
dbed05ca
         args=''
3b06ab84
     elif args.startswith("{"):
         # Argument is a YAML document (not a file)
2f51f3bb
         complex_args = utils_vars.combine_vars(complex_args, loader.load(args))
3b06ab84
         args=''
dbed05ca
 
53ae3266
     if args:
         parsed_args = parse_kv(args)
         complex_args = utils_vars.combine_vars(complex_args, parsed_args)
 
08208285
     task_vars = interpreters
b8cbf137
 
     if check:
08208285
         complex_args['_ansible_check_mode'] = True
b8cbf137
 
4b0aa121
     modname = os.path.basename(modfile)
     modname = os.path.splitext(modname)[0]
3c7a502c
     (module_data, module_style, shebang) = module_common.modify_module(
4b0aa121
         modname,
53ae3266
         modfile,
d34a26e3
         complex_args,
53ae3266
         task_vars=task_vars
d34a26e3
     )
af2fb56a
 
48a27734
     if module_style == 'new' and 'ANSIBALLZ_WRAPPER = True' in module_data:
         module_style = 'ansiballz'
bdd73e31
 
b05485d4
     modfile2_path = os.path.expanduser(destfile)
b8c9391d
     print("* including generated source, if any, saving to: %s" % modfile2_path)
48a27734
     if module_style not in ('ansiballz', 'old'):
bdd73e31
         print("* this may offset any line numbers in tracebacks/debuggers!")
d34a26e3
     modfile2 = open(modfile2_path, 'w')
     modfile2.write(module_data)
     modfile2.close()
     modfile = modfile2_path
9858b1f2
 
bdd73e31
     return (modfile2_path, modname, module_style)
 
08208285
 
 def ansiballz_setup(modfile, modname, interpreters):
bdd73e31
     os.system("chmod +x %s" % modfile)
 
08208285
     if 'ansible_python_interpreter' in interpreters:
         command = [interpreters['ansible_python_interpreter']]
     else:
         command = []
     command.extend([modfile, 'explode'])
 
     cmd = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
bdd73e31
     out, err = cmd.communicate()
     lines = out.splitlines()
     if len(lines) != 2 or 'Module expanded into' not in lines[0]:
         print("*" * 35)
48a27734
         print("INVALID OUTPUT FROM ANSIBALLZ MODULE WRAPPER")
bdd73e31
         print(out)
         sys.exit(1)
     debug_dir = lines[1].strip()
 
     argsfile = os.path.join(debug_dir, 'args')
     modfile = os.path.join(debug_dir, 'ansible_module_%s.py' % modname)
 
48a27734
     print("* ansiballz module detected; extracted module source to: %s" % debug_dir)
bdd73e31
     return modfile, argsfile
ade0233d
 
08208285
 
 def runtest(modfile, argspath, modname, module_style, interpreters):
e8583833
     """Test run a module, piping it's output for reporting."""
ba39d115
     invoke = ""
48a27734
     if module_style == 'ansiballz':
08208285
         modfile, argspath = ansiballz_setup(modfile, modname, interpreters)
         if 'ansible_python_interpreter' in interpreters:
             invoke = "%s " % interpreters['ansible_python_interpreter']
fc96b882
 
ade0233d
     os.system("chmod +x %s" % modfile)
fc96b882
 
08208285
     invoke = "%s%s" % (invoke, modfile)
b8c9391d
     if argspath is not None:
08208285
         invoke = "%s %s" % (invoke, argspath)
fc96b882
 
     cmd = subprocess.Popen(invoke, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ade0233d
     (out, err) = cmd.communicate()
 
     try:
bdd73e31
         print("*" * 35)
b8c9391d
         print("RAW OUTPUT")
         print(out)
         print(err)
ea6ec3bf
         results = json.loads(out)
ade0233d
     except:
bdd73e31
         print("*" * 35)
b8c9391d
         print("INVALID OUTPUT FORMAT")
         print(out)
ade0233d
         traceback.print_exc()
         sys.exit(1)
be55145a
 
bdd73e31
     print("*" * 35)
b8c9391d
     print("PARSED OUTPUT")
     print(jsonify(results,format=True))
a735dd2b
 
08208285
 
bdd73e31
 def rundebug(debugger, modfile, argspath, modname, module_style):
e8583833
     """Run interactively with console debugger."""
 
48a27734
     if module_style == 'ansiballz':
         modfile, argspath = ansiballz_setup(modfile, modname)
bdd73e31
 
fc96b882
     if argspath is not None:
         subprocess.call("%s %s %s" % (debugger, modfile, argspath), shell=True)
     else:
         subprocess.call("%s %s" % (debugger, modfile), shell=True)
e8583833
 
08208285
 
b8c9391d
 def main():
e8583833
 
fc96b882
     options, args = parse()
08208285
     interpreters = get_interpreters(options.interpreter)
     (modfile, modname, module_style) = boilerplate_module(options.module_path, options.module_args, interpreters, options.check, options.filename)
af2fb56a
 
3b0524e6
     argspath = None
48a27734
     if module_style not in ('new', 'ansiballz'):
d34a26e3
         if module_style == 'non_native_want_json':
af2fb56a
             argspath = write_argsfile(options.module_args, json=True)
d34a26e3
         elif module_style == 'old':
d154bf87
             argspath = write_argsfile(options.module_args, json=False)
d34a26e3
         else:
             raise Exception("internal error, unexpected module style: %s" % module_style)
08208285
 
b05485d4
     if options.execute:
8342cc6b
         if options.debugger:
bdd73e31
             rundebug(options.debugger, modfile, argspath, modname, module_style)
b05485d4
         else:
08208285
             runtest(modfile, argspath, modname, module_style, interpreters)
b8c9391d
 
ade0233d
 if __name__ == "__main__":
bdd73e31
     try:
         main()
     finally:
         shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)