diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py new file mode 100644 index 00000000000..4d6fde47441 --- /dev/null +++ b/lib/ansible/module_common.py @@ -0,0 +1,175 @@ +# (c) 2013, 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 . + +# from python and deps +from cStringIO import StringIO +import inspect +import os +import jinja2 +import shlex + +# from Ansible +from ansible import errors +from ansible import utils +from ansible import constants as C + +REPLACER = "#<>" +REPLACER_ARGS = "\"<>\"" +REPLACER_LANG = "\"<>\"" +REPLACER_COMPLEX = "\"<>\"" + +class ModuleReplacer(object): + + """ + The Replacer is used to insert chunks of code into modules before + transfer. Rather than doing classical python imports, this allows for more + efficient transfer in a no-bootstrapping scenario by not moving extra files + over the wire, and also takes care of embedding arguments in the transferred + modules. + + This version is done in such a way that local imports can still be + used in the module code, so IDEs don't have to be aware of what is going on. + + Example: + + from ansible.module_utils.basic import * + + will result in a template evaluation of + + {{ include 'basic.py' }} + + from the module_utils/ directory in the source tree. + + All modules are required to import at least basic, though there will also + be other snippets. + """ + + # ****************************************************************************** + + def __init__(self, strip_comments=False): + this_file = inspect.getfile(inspect.currentframe()) + self.snippet_path = os.path.join(os.path.dirname(this_file), 'module_utils') + self.strip_comments = strip_comments # TODO: implement + + # ****************************************************************************** + + def _find_snippet_imports(self, module_data, module_path): + """ + Given the source of the module, convert it to a Jinja2 template to insert + module code and return whether it's a new or old style module. + """ + + module_style = 'old' + if REPLACER in module_data: + module_style = 'new' + elif 'from ansible.snippets.' in module_data: + module_style = 'new' + elif 'WANT_JSON' in module_data: + module_style = 'non_native_want_json' + + output = StringIO() + lines = module_data.split('\n') + snippet_names = [] + + for line in lines: + + if line.find(REPLACER) != -1: + output.write("{% include 'basic.py' %}\n\n") + snippet_names.append('basic') + elif line.startswith('from ansible.module_utils.'): + tokens=line.split(".") + import_error = False + if len(tokens) != 3: + import_error = True + if line.find(" import *") == -1: + import_error = True + if import_error: + raise errors.AnsibleError("error importing module in %s, expecting format like 'from ansible.module_utils.basic import *'" % module_path) + snippet_name = tokens[2].split()[0] + snippet_names.append(snippet_name) + output.write("{% include '" + snippet_name + ".py' %}\n\n") + else: + if self.strip_comments and line.startswith("#") or line == '': + pass + output.write(line) + output.write("\n") + + if len(snippet_names) > 0 and not 'basic' in snippet_names: + raise errors.AnsibleError("missing required import in %s: from ansible.module_utils.basic import *" % module_path) + + return (output.getvalue(), module_style) + + # ****************************************************************************** + + def _template_imports(self, module_data): + + loader = jinja2.FileSystemLoader([self.snippet_path]) + environment = jinja2.Environment(loader=loader) # trim_blocks=True) + t = environment.from_string(module_data) + vars_input = {} + try: + return t.render(vars_input) + except jinja2.TemplateNotFound, tnf: + raise errors.AnsibleError("failure to find one of the imported module utilities, an ansible.module_utils import is likely referencing a module that does not exist. Original exception was: %s" % str(tnf)) + + + + # ****************************************************************************** + + def modify_module(self, module_path, complex_args, module_args, inject): + + with open(module_path) as f: + + # read in the module source + module_data = f.read() + + (module_data, module_style) = self._find_snippet_imports(module_data, module_path) + module_data = self._template_imports(module_data) + + complex_args_json = utils.jsonify(complex_args) + # We force conversion of module_args to str because module_common calls shlex.split, + # a standard library function that incorrectly handles Unicode input before Python 2.7.3. + encoded_args = repr(module_args.encode('utf-8')) + encoded_lang = repr(C.DEFAULT_MODULE_LANG) + encoded_complex = repr(complex_args_json) + + # these strings should be part of the 'basic' snippet which is required to be included + module_data = module_data.replace(REPLACER_ARGS, encoded_args) + module_data = module_data.replace(REPLACER_LANG, encoded_lang) + module_data = module_data.replace(REPLACER_COMPLEX, encoded_complex) + + if module_style == 'new': + facility = C.DEFAULT_SYSLOG_FACILITY + if 'ansible_syslog_facility' in inject: + facility = inject['ansible_syslog_facility'] + module_data = module_data.replace('syslog.LOG_USER', "syslog.%s" % facility) + + + lines = module_data.split("\n") + shebang = None + if lines[0].startswith("#!"): + shebang = lines[0].strip() + args = shlex.split(str(shebang[2:])) + interpreter = args[0] + interpreter_config = 'ansible_%s_interpreter' % os.path.basename(interpreter) + + if interpreter_config in inject: + lines[0] = shebang = "#!%s %s" % (inject[interpreter_config], " ".join(args[1:])) + module_data = "\n".join(lines) + + return (module_data, module_style, shebang) +