From c8ac97657e1f124df654d6c492fdcea0e568cfcc Mon Sep 17 00:00:00 2001 From: Mumshad Mannambeth Date: Tue, 27 Jun 2017 23:27:22 -0400 Subject: [PATCH] Add support for custom modules --- Dockerfile | 2 + helpers/module_template.py | 72 +++++ helpers/test-module | 245 ++++++++++++++++++ .../custom_module/custom_module.controller.js | 39 ++- server/api/custom_module/index.spec.js | 16 +- server/config/environment/development.js | 2 +- 6 files changed, 354 insertions(+), 22 deletions(-) create mode 100644 helpers/module_template.py create mode 100644 helpers/test-module diff --git a/Dockerfile b/Dockerfile index c21a06c..5f0caf9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,3 +58,5 @@ USER root RUN chown -R app_user /data/web-app ENTRYPOINT gulp serve + +COPY helpers/module_template.py /opt/ehc-builder-scripts/ansible_modules/template.py diff --git a/helpers/module_template.py b/helpers/module_template.py new file mode 100644 index 0000000..69502be --- /dev/null +++ b/helpers/module_template.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +try: + import json +except ImportError: + import simplejson as json + +DOCUMENTATION = ''' +--- +module: module_name +short_description: shoort description of the module +description: + - Long Description of the module +options: + parameter1: + description: + - Description of option 1 + required: true + default: null + aliases: [] + parameter2: + description: + - Description of parameter2 + required: true + default: null + aliases: [] + parameter3: + description: + - Description of parameter3 + required: true + default: null +''' + +EXAMPLES = ''' +# Example +module_name: + parameter1: value1 + parameter2: value2 + parameter3: + key1: value1 + key2: value2 +''' + +def main(): + module = AnsibleModule( + argument_spec = dict( + # <--Begin Parameter Definition --> + parameter1=dict(required=True), + parameter2=dict(required=True, type='bool'), + parameter3=dict(required=True, type='dict') + # <--END Parameter Definition --> + ) + # <--Begin Supports Check Mode --> + # <--End Supports Check Mode --> + ) + + # <--Begin Retreiving Parameters --> + parameter1 = module.params['parameter1'] + parameter2 = module.params['parameter2'] + parameter3 = module.params['parameter3'] + # <--End Retreiving Parameters --> + + # Successfull Exit + module.exit_json(changed=True, msg="Success Message") + + # Fail Exit + module.fail_json(msg="Error Message") + + +from ansible.module_utils.basic import AnsibleModule +if __name__ == '__main__': + main() diff --git a/helpers/test-module b/helpers/test-module new file mode 100644 index 0000000..717b9bc --- /dev/null +++ b/helpers/test-module @@ -0,0 +1,245 @@ +#!/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 +import optparse +import os +import subprocess +import sys +import traceback +import shutil +import pydevd + +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] + (module_data, module_style, shebang) = module_common.modify_module( + modname, + modfile, + complex_args, + task_vars=task_vars + ) + + if module_style == 'new' and 'ANSIBALLZ_WRAPPER = True' in module_data: + module_style = 'ansiballz' + + modfile2_path = os.path.expanduser(destfile) + print("* including generated source, if any, saving to: %s" % modfile2_path) + if module_style not in ('ansiballz', 'old'): + 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, modname, module_style) + +def ansiballz_setup(modfile, modname): + os.system("chmod +x %s" % modfile) + + cmd = subprocess.Popen([modfile, 'explode'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = cmd.communicate() + lines = out.splitlines() + if len(lines) != 2 or 'Module expanded into' not in lines[0]: + print("*" * 35) + print("INVALID OUTPUT FROM ANSIBALLZ MODULE WRAPPER") + 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) + + print("* ansiballz module detected; extracted module source to: %s" % debug_dir) + return modfile, argsfile + +def runtest(modfile, argspath, modname, module_style): + """Test run a module, piping it's output for reporting.""" + if module_style == 'ansiballz': + modfile, argspath = ansiballz_setup(modfile, modname) + + os.system("chmod +x %s" % modfile) + + invoke = "%s" % (modfile) + if argspath is not None: + invoke = "%s %s" % (modfile, argspath) + #pydevd.settrace('10.252.40.157', port=53483, stdoutToServer=True, stderrToServer=True) + cmd = subprocess.Popen(invoke, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = cmd.communicate() + + try: + print("*" * 35) + print("RAW OUTPUT") + print(out) + print(err) + results = json.loads(out) + except: + print("*" * 35) + print("INVALID OUTPUT FORMAT") + print(out) + traceback.print_exc() + sys.exit(1) + + print("*" * 35) + print("PARSED OUTPUT") + print(jsonify(results,format=True)) + +def rundebug(debugger, modfile, argspath, modname, module_style): + """Run interactively with console debugger.""" + + if module_style == 'ansiballz': + modfile, argspath = ansiballz_setup(modfile, modname) + + 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, modname, module_style) = boilerplate_module(options.module_path, options.module_args, options.interpreter, options.check, options.filename) + + argspath = None + if module_style not in ('new', 'ansiballz'): + 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, modname, module_style) + else: + runtest(modfile, argspath, modname, module_style) + +if __name__ == "__main__": + try: + main() + finally: + shutil.rmtree(C.DEFAULT_LOCAL_TMP, True) + diff --git a/server/api/custom_module/custom_module.controller.js b/server/api/custom_module/custom_module.controller.js index d4019f9..757cf83 100644 --- a/server/api/custom_module/custom_module.controller.js +++ b/server/api/custom_module/custom_module.controller.js @@ -105,7 +105,13 @@ export function show(req, res) { var command = 'cat "' + ansibleEngine.customModules + '"/' + req.params.custom_module; if(req.params.custom_module === 'template.py'){ - command = 'cat ' + '/opt/ehc-builder-scripts/ansible_modules/template.py'; + //command = 'cat ' + '/opt/ehc-builder-scripts/ansible_modules/template.py'; + + return require('fs').readFile('./helpers/module_template.py', (err, data) => { + if (err) res.status(500).send(data); + res.send(data); + }); + } @@ -137,20 +143,27 @@ export function testModule(req, res) { res.status(500).send("Custom Modules Folder not defined in Ansible Engine") } - var command = '/opt/ansible/ansible-devel/hacking/test-module -m "' + ansibleEngine.customModules + '/' + req.params.custom_module + "\" -a '" + JSON.stringify(moduleArgs) + "'"; + var test_module = '/tmp/test-module'; - console.log("Command=" + command); + var command = 'chmod 755 ' + test_module + '; ' + test_module + ' -m "' + ansibleEngine.customModules + '/' + req.params.custom_module + "\" -a '" + JSON.stringify(moduleArgs) + "'"; + + scp2_exec.copyFileToScriptEngine('./helpers/test-module',test_module,ansibleEngine,function(){ + console.log("Command=" + command); + + ssh2_exec.executeCommand(command, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + }, function(errorResponse){ + res.status(500).send(errorResponse) + }); - ssh2_exec.executeCommand(command, - null, - function(data){ - res.send(data); - }, - function(data){ - res.status(500).send(data) - }, - ansibleEngine - ); /*return CustomModule.findById(req.params.custom_module).exec() .then(handleEntityNotFound(res)) diff --git a/server/api/custom_module/index.spec.js b/server/api/custom_module/index.spec.js index 04b1873..08d2cf8 100644 --- a/server/api/custom_module/index.spec.js +++ b/server/api/custom_module/index.spec.js @@ -36,26 +36,26 @@ describe('CustomModule API Router:', function() { expect(customModuleIndex).to.equal(routerStub); }); - describe('GET /api/custom_modules', function() { + describe('GET /api/custom_modules/query', function() { it('should route to customModule.controller.index', function() { - expect(routerStub.get - .withArgs('/', 'customModuleCtrl.index') + expect(routerStub.post + .withArgs('/query', 'customModuleCtrl.index') ).to.have.been.calledOnce; }); }); - describe('GET /api/custom_modules/:id', function() { + describe('GET /api/custom_modules/:custom_module/get', function() { it('should route to customModule.controller.show', function() { - expect(routerStub.get - .withArgs('/:id', 'customModuleCtrl.show') + expect(routerStub.post + .withArgs('/:custom_module/get', 'customModuleCtrl.show') ).to.have.been.calledOnce; }); }); - describe('POST /api/custom_modules', function() { + describe('POST /api/custom_modules/:custom_module', function() { it('should route to customModule.controller.create', function() { expect(routerStub.post - .withArgs('/', 'customModuleCtrl.create') + .withArgs('/:custom_module', 'customModuleCtrl.create') ).to.have.been.calledOnce; }); }); diff --git a/server/config/environment/development.js b/server/config/environment/development.js index cb08353..046305b 100644 --- a/server/config/environment/development.js +++ b/server/config/environment/development.js @@ -11,6 +11,6 @@ module.exports = { }, // Seed database on startup - seedDB: true + seedDB: false };