2014-11-14 22:14:08 +00:00
|
|
|
# (c) 2013-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
2015-02-10 20:35:34 +00:00
|
|
|
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
|
2014-11-14 22:14:08 +00:00
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
# Make coding more python3-ish
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
import base64
|
2014-11-14 22:14:08 +00:00
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import shlex
|
2016-04-05 18:06:17 +00:00
|
|
|
import zipfile
|
|
|
|
from io import BytesIO
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
# from Ansible
|
|
|
|
from ansible import __version__
|
|
|
|
from ansible import constants as C
|
|
|
|
from ansible.errors import AnsibleError
|
2016-02-27 00:41:13 +00:00
|
|
|
from ansible.utils.unicode import to_bytes, to_unicode
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
try:
|
|
|
|
from __main__ import display
|
|
|
|
except ImportError:
|
|
|
|
from ansible.utils.display import Display
|
|
|
|
display = Display()
|
|
|
|
|
2016-02-27 00:41:13 +00:00
|
|
|
REPLACER = b"#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
|
2016-04-05 18:06:17 +00:00
|
|
|
REPLACER_VERSION = b"\"<<ANSIBLE_VERSION>>\""
|
2016-02-27 00:41:13 +00:00
|
|
|
REPLACER_COMPLEX = b"\"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>\""
|
|
|
|
REPLACER_WINDOWS = b"# POWERSHELL_COMMON"
|
|
|
|
REPLACER_JSONARGS = b"<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>"
|
|
|
|
REPLACER_SELINUX = b"<<SELINUX_SPECIAL_FILESYSTEMS>>"
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
# We could end up writing out parameters with unicode characters so we need to
|
|
|
|
# specify an encoding for the python source file
|
2016-02-27 00:41:13 +00:00
|
|
|
ENCODING_STRING = b'# -*- coding: utf-8 -*-'
|
2015-02-10 20:35:34 +00:00
|
|
|
|
|
|
|
# we've moved the module_common relative to the snippets, so fix the path
|
2015-02-10 22:41:15 +00:00
|
|
|
_SNIPPET_PATH = os.path.join(os.path.dirname(__file__), '..', 'module_utils')
|
2015-02-10 20:35:34 +00:00
|
|
|
|
|
|
|
# ******************************************************************************
|
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
ZIPLOADER_TEMPLATE = u'''%(shebang)s
|
|
|
|
# -*- coding: utf-8 -*-'
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import base64
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
ZIPDATA = """%(zipdata)s"""
|
|
|
|
|
|
|
|
def debug(command, zipped_mod):
|
|
|
|
# The code here normally doesn't run. It's only used for debugging on the
|
|
|
|
# remote machine. Run with ANSIBLE_KEEP_REMOTE_FILES=1 envvar and -vvv
|
|
|
|
# to save the module file remotely. Login to the remote machine and use
|
|
|
|
# /path/to/module explode to extract the ZIPDATA payload into source
|
|
|
|
# files. Edit the source files to instrument the code or experiment with
|
|
|
|
# different values. Then use /path/to/module execute to run the extracted
|
|
|
|
# files you've edited instead of the actual zipped module.
|
|
|
|
#
|
|
|
|
# Okay to use __file__ here because we're running from a kept file
|
|
|
|
basedir = os.path.dirname(__file__)
|
|
|
|
if command == 'explode':
|
|
|
|
import zipfile
|
|
|
|
z = zipfile.ZipFile(zipped_mod)
|
|
|
|
for filename in z.namelist():
|
|
|
|
if filename.startswith('/'):
|
|
|
|
raise Exception('Something wrong with this module zip file: should not contain absolute paths')
|
|
|
|
dest_filename = os.path.join(basedir, filename)
|
|
|
|
if dest_filename.endswith(os.path.sep) and not os.path.exists(dest_filename):
|
|
|
|
os.makedirs(dest_filename)
|
|
|
|
else:
|
|
|
|
directory = os.path.dirname(dest_filename)
|
|
|
|
if not os.path.exists(directory):
|
|
|
|
os.makedirs(directory)
|
|
|
|
f = open(dest_filename, 'w')
|
|
|
|
f.write(z.read(filename))
|
|
|
|
f.close()
|
|
|
|
print('Module expanded into: %%s' %% os.path.join(basedir, 'ansible'))
|
|
|
|
elif command == 'execute':
|
|
|
|
sys.path.insert(0, basedir)
|
|
|
|
from ansible.module_exec.%(ansible_module)s.__main__ import main
|
|
|
|
main()
|
|
|
|
|
|
|
|
os.environ['ANSIBLE_MODULE_ARGS'] = %(args)s
|
|
|
|
os.environ['ANSIBLE_MODULE_CONSTANTS'] = %(constants)s
|
|
|
|
|
|
|
|
try:
|
|
|
|
temp_fd, temp_path = tempfile.mkstemp(prefix='ansible_')
|
|
|
|
os.write(temp_fd, base64.b64decode(ZIPDATA))
|
|
|
|
if len(sys.argv) == 2:
|
|
|
|
debug(sys.argv[1], temp_path)
|
|
|
|
else:
|
|
|
|
sys.path.insert(0, temp_path)
|
|
|
|
from ansible.module_exec.%(ansible_module)s.__main__ import main
|
|
|
|
main()
|
|
|
|
finally:
|
|
|
|
try:
|
|
|
|
os.close(temp_fd)
|
|
|
|
os.remove(temp_path)
|
|
|
|
except NameError:
|
|
|
|
# mkstemp failed
|
|
|
|
pass
|
|
|
|
'''
|
|
|
|
|
|
|
|
def _strip_comments(source):
|
|
|
|
# Strip comments and blank lines from the wrapper
|
|
|
|
buf = []
|
|
|
|
for line in source.splitlines():
|
|
|
|
l = line.strip()
|
|
|
|
if not l or l.startswith(u'#'):
|
|
|
|
continue
|
|
|
|
buf.append(line)
|
|
|
|
return u'\n'.join(buf)
|
|
|
|
|
|
|
|
# ZIPLOADER_TEMPLATE stripped of comments for smaller over the wire size
|
|
|
|
STRIPPED_ZIPLOADER_TEMPLATE = _strip_comments(ZIPLOADER_TEMPLATE)
|
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
def _slurp(path):
|
|
|
|
if not os.path.exists(path):
|
|
|
|
raise AnsibleError("imported module support code does not exist at %s" % path)
|
2016-02-27 00:41:13 +00:00
|
|
|
fd = open(path, 'rb')
|
2015-02-10 20:35:34 +00:00
|
|
|
data = fd.read()
|
|
|
|
fd.close()
|
|
|
|
return data
|
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
def _get_shebang(interpreter, task_vars, args=tuple()):
|
|
|
|
"""
|
|
|
|
Note not stellar API:
|
|
|
|
Returns None instead of always returning a shebang line. Doing it this
|
|
|
|
way allows the caller to decide to use the shebang it read from the
|
|
|
|
file rather than trust that we reformatted what they already have
|
|
|
|
correctly.
|
|
|
|
"""
|
|
|
|
interpreter_config = u'ansible_%s_interpreter' % os.path.basename(interpreter)
|
|
|
|
|
|
|
|
if interpreter_config not in task_vars:
|
|
|
|
return None
|
|
|
|
|
|
|
|
interpreter = task_vars[interpreter_config]
|
|
|
|
shebang = u'#!' + interpreter
|
|
|
|
|
|
|
|
if args:
|
|
|
|
shebang = shebang + u' ' + u' '.join(args)
|
|
|
|
|
|
|
|
return shebang
|
|
|
|
|
|
|
|
def _get_facility(task_vars):
|
|
|
|
facility = C.DEFAULT_SYSLOG_FACILITY
|
|
|
|
if 'ansible_syslog_facility' in task_vars:
|
|
|
|
facility = task_vars['ansible_syslog_facility']
|
|
|
|
return facility
|
|
|
|
|
|
|
|
def _find_snippet_imports(module_name, module_data, module_path, module_args, task_vars, module_compression):
|
2015-02-10 20:35:34 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
module_substyle = module_style = 'old'
|
|
|
|
|
|
|
|
# module_style is something important to calling code (ActionBase). It
|
|
|
|
# determines how arguments are formatted (json vs k=v) and whether
|
|
|
|
# a separate arguments file needs to be sent over the wire.
|
|
|
|
# module_substyle is extra information that's useful internally. It tells
|
|
|
|
# us what we have to look to substitute in the module files and whether
|
|
|
|
# we're using module replacer or ziploader to format the module itself.
|
2015-02-10 20:35:34 +00:00
|
|
|
if REPLACER in module_data:
|
2016-04-05 18:06:17 +00:00
|
|
|
# Do REPLACER before from ansible.module_utils because we need make sure
|
|
|
|
# we substitute "from ansible.module_utils basic" for REPLACER
|
|
|
|
module_style = 'new'
|
|
|
|
module_substyle = 'python'
|
|
|
|
module_data = module_data.replace(REPLACER, b'from ansible.module_utils.basic import *')
|
|
|
|
elif b'from ansible.module_utils.' in module_data:
|
2015-02-10 20:35:34 +00:00
|
|
|
module_style = 'new'
|
2016-04-05 18:06:17 +00:00
|
|
|
module_substyle = 'python'
|
2015-07-24 16:39:54 +00:00
|
|
|
elif REPLACER_WINDOWS in module_data:
|
|
|
|
module_style = 'new'
|
2016-04-05 18:06:17 +00:00
|
|
|
module_substyle = 'powershell'
|
2015-09-07 03:01:26 +00:00
|
|
|
elif REPLACER_JSONARGS in module_data:
|
|
|
|
module_style = 'new'
|
2016-04-05 18:06:17 +00:00
|
|
|
module_substyle = 'jsonargs'
|
2016-02-27 00:41:13 +00:00
|
|
|
elif b'WANT_JSON' in module_data:
|
2016-04-05 18:06:17 +00:00
|
|
|
module_substyle = module_style = 'non_native_want_json'
|
|
|
|
|
|
|
|
shebang = None
|
|
|
|
# Neither old-style nor non_native_want_json modules should be modified
|
|
|
|
# except for the shebang line (Done by modify_module)
|
|
|
|
if module_style in ('old', 'non_native_want_json'):
|
|
|
|
return module_data, module_style, shebang
|
|
|
|
|
|
|
|
module_args_json = to_bytes(json.dumps(module_args))
|
2015-02-10 20:35:34 +00:00
|
|
|
|
2016-02-27 00:41:13 +00:00
|
|
|
output = BytesIO()
|
|
|
|
lines = module_data.split(b'\n')
|
2016-04-05 18:06:17 +00:00
|
|
|
|
|
|
|
snippet_names = set()
|
|
|
|
|
|
|
|
if module_substyle == 'python':
|
|
|
|
# ziploader for new-style python classes
|
|
|
|
python_repred_args = to_bytes(repr(module_args_json))
|
|
|
|
constants = dict(
|
|
|
|
SELINUX_SPECIAL_FS=C.DEFAULT_SELINUX_SPECIAL_FS,
|
|
|
|
SYSLOG_FACILITY=_get_facility(task_vars),
|
|
|
|
)
|
|
|
|
python_repred_constants = to_bytes(repr(json.dumps(constants)), errors='strict')
|
|
|
|
|
|
|
|
try:
|
|
|
|
compression_method = getattr(zipfile, module_compression)
|
|
|
|
except AttributeError:
|
|
|
|
display.warning(u'Bad module compression string specified: %s. Using ZIP_STORED (no compression)' % module_compression)
|
|
|
|
compression_method = zipfile.ZIP_STORED
|
|
|
|
zipoutput = BytesIO()
|
|
|
|
zf = zipfile.ZipFile(zipoutput, mode='w', compression=compression_method)
|
|
|
|
zf.writestr('ansible/__init__.py', b''.join((b"__version__ = '", to_bytes(__version__), b"'\n")))
|
|
|
|
zf.writestr('ansible/module_utils/__init__.py', b'')
|
|
|
|
zf.writestr('ansible/module_exec/__init__.py', b'')
|
|
|
|
|
|
|
|
zf.writestr('ansible/module_exec/%s/__init__.py' % module_name, b"")
|
|
|
|
final_data = []
|
|
|
|
|
|
|
|
for line in lines:
|
|
|
|
if line.startswith(b'from ansible.module_utils.'):
|
|
|
|
tokens=line.split(b".")
|
|
|
|
snippet_name = tokens[2].split()[0]
|
|
|
|
snippet_names.add(snippet_name)
|
|
|
|
fname = to_unicode(snippet_name + b".py")
|
|
|
|
zf.writestr(os.path.join("ansible/module_utils", fname), _slurp(os.path.join(_SNIPPET_PATH, fname)))
|
|
|
|
final_data.append(line)
|
|
|
|
else:
|
|
|
|
final_data.append(line)
|
|
|
|
|
|
|
|
zf.writestr('ansible/module_exec/%s/__main__.py' % module_name, b"\n".join(final_data))
|
|
|
|
zf.close()
|
|
|
|
shebang = _get_shebang(u'/usr/bin/python', task_vars) or u'#!/usr/bin/python'
|
|
|
|
output.write(to_bytes(STRIPPED_ZIPLOADER_TEMPLATE % dict(
|
|
|
|
zipdata=base64.b64encode(zipoutput.getvalue()),
|
|
|
|
ansible_module=module_name,
|
|
|
|
args=python_repred_args,
|
|
|
|
constants=python_repred_constants,
|
|
|
|
shebang=shebang,
|
|
|
|
)))
|
|
|
|
module_data = output.getvalue()
|
|
|
|
|
|
|
|
# Sanity check from 1.x days. Maybe too strict. Some custom python
|
|
|
|
# modules that use ziploader may implement their own helpers and not
|
|
|
|
# need basic.py. All the constants that we substituted into basic.py
|
|
|
|
# for module_replacer are now available in other, better ways.
|
|
|
|
if b'basic' not in snippet_names:
|
|
|
|
raise AnsibleError("missing required import in %s: Did not import ansible.module_utils.basic for boilerplate helper code" % module_path)
|
|
|
|
|
|
|
|
elif module_substyle == 'powershell':
|
|
|
|
# Module replacer for jsonargs and windows
|
|
|
|
for line in lines:
|
|
|
|
if REPLACER_WINDOWS in line:
|
|
|
|
ps_data = _slurp(os.path.join(_SNIPPET_PATH, "powershell.ps1"))
|
|
|
|
output.write(ps_data)
|
|
|
|
snippet_names.add(b'powershell')
|
|
|
|
continue
|
|
|
|
output.write(line + b'\n')
|
|
|
|
module_data = output.getvalue()
|
|
|
|
module_data = module_data.replace(REPLACER_JSONARGS, module_args_json)
|
|
|
|
|
|
|
|
# Sanity check from 1.x days. This is currently useless as we only
|
|
|
|
# get here if we are going to substitute powershell.ps1 into the
|
|
|
|
# module anyway. Leaving it for when/if we add other powershell
|
|
|
|
# module_utils files.
|
|
|
|
if b'powershell' not in snippet_names:
|
2015-02-10 20:35:34 +00:00
|
|
|
raise AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path)
|
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
elif module_substyle == 'jsonargs':
|
|
|
|
# these strings could be included in a third-party module but
|
|
|
|
# officially they were included in the 'basic' snippet for new-style
|
|
|
|
# python modules (which has been replaced with something else in
|
|
|
|
# ziploader) If we remove them from jsonargs-style module replacer
|
|
|
|
# then we can remove them everywhere.
|
|
|
|
module_data = module_data.replace(REPLACER_VERSION, to_bytes(repr(__version__)))
|
|
|
|
module_data = module_data.replace(REPLACER_COMPLEX, python_repred_args)
|
|
|
|
module_data = module_data.replace(REPLACER_SELINUX, to_bytes(','.join(C.DEFAULT_SELINUX_SPECIAL_FS)))
|
|
|
|
|
|
|
|
# The main event -- substitute the JSON args string into the module
|
|
|
|
module_data = module_data.replace(REPLACER_JSONARGS, module_args_json)
|
|
|
|
|
|
|
|
facility = b'syslog.' + to_bytes(_get_facility(task_vars), errors='strict')
|
|
|
|
module_data = module_data.replace(b'syslog.LOG_USER', facility)
|
|
|
|
|
|
|
|
return (module_data, module_style, shebang)
|
2015-02-10 20:35:34 +00:00
|
|
|
|
|
|
|
# ******************************************************************************
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
def modify_module(module_name, module_path, module_args, task_vars=dict(), module_compression='ZIP_STORED'):
|
2014-11-14 22:14:08 +00:00
|
|
|
"""
|
2015-02-10 20:35:34 +00:00
|
|
|
Used to insert chunks of code into modules before transfer rather than
|
|
|
|
doing regular python imports. This allows for more efficient transfer in
|
|
|
|
a non-bootstrapping scenario by not moving extra files over the wire and
|
|
|
|
also takes care of embedding arguments in the transferred modules.
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
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:
|
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
from ansible.module_utils.basic import *
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
... will result in the insertion of basic.py into the module
|
|
|
|
from the module_utils/ directory in the source tree.
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
All modules are required to import at least basic, though there will also
|
|
|
|
be other snippets.
|
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
For powershell, there's equivalent conventions like this:
|
|
|
|
|
2014-11-14 22:14:08 +00:00
|
|
|
# POWERSHELL_COMMON
|
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
which results in the inclusion of the common code from powershell.ps1
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
"""
|
2015-03-27 14:48:26 +00:00
|
|
|
### TODO: Optimization ideas if this code is actually a source of slowness:
|
|
|
|
# * Fix comment stripping: Currently doesn't preserve shebangs and encoding info (but we unconditionally add encoding info)
|
|
|
|
# * Use pyminifier if installed
|
|
|
|
# * comment stripping/pyminifier needs to have config setting to turn it
|
|
|
|
# off for debugging purposes (goes along with keep remote but should be
|
|
|
|
# separate otherwise users wouldn't be able to get info on what the
|
|
|
|
# minifier output)
|
|
|
|
# * Only split into lines and recombine into strings once
|
|
|
|
# * Cache the modified module? If only the args are different and we do
|
2015-09-21 08:46:29 +00:00
|
|
|
# that as the last step we could cache all the work up to that point.
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2016-02-27 00:41:13 +00:00
|
|
|
with open(module_path, 'rb') as f:
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2015-02-10 20:35:34 +00:00
|
|
|
# read in the module source
|
|
|
|
module_data = f.read()
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
(module_data, module_style, shebang) = _find_snippet_imports(module_name, module_data, module_path, module_args, task_vars, module_compression)
|
2015-09-21 08:46:29 +00:00
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
if shebang is None:
|
|
|
|
lines = module_data.split(b"\n", 1)
|
|
|
|
if lines[0].startswith(b"#!"):
|
|
|
|
shebang = lines[0].strip()
|
|
|
|
args = shlex.split(str(shebang[2:]))
|
|
|
|
interpreter = args[0]
|
|
|
|
interpreter = to_bytes(interpreter)
|
|
|
|
|
|
|
|
new_shebang = to_bytes(_get_shebang(interpreter, task_vars, args[1:]), errors='strict', nonstring='passthru')
|
|
|
|
if new_shebang:
|
|
|
|
lines[0] = shebang = new_shebang
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2016-04-05 18:06:17 +00:00
|
|
|
if os.path.basename(interpreter).startswith(b'python'):
|
|
|
|
lines.insert(1, ENCODING_STRING)
|
|
|
|
else:
|
|
|
|
# No shebang, assume a binary module?
|
|
|
|
pass
|
|
|
|
|
|
|
|
module_data = b"\n".join(lines)
|
|
|
|
else:
|
|
|
|
shebang = to_bytes(shebang, errors='strict')
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2015-09-21 08:46:29 +00:00
|
|
|
return (module_data, module_style, shebang)
|