Move module arg passing from the environment to stdin (from the wrapper to the module)

This commit is contained in:
Toshio Kuratomi 2016-04-10 12:33:16 -07:00
parent dcc5dfdf81
commit b571ecdfec
2 changed files with 111 additions and 88 deletions

View file

@ -97,9 +97,35 @@ if sys.version_info < (3,):
else: else:
unicode = str unicode = str
try:
# Python-2.6+
from io import BytesIO as IOStream
except ImportError:
# Python < 2.6
from StringIO import StringIO as IOStream
ZIPDATA = """%(zipdata)s""" ZIPDATA = """%(zipdata)s"""
def debug(command, zipped_mod): def invoke_module(module_path, json_params):
pythonpath = os.environ.get('PYTHONPATH')
if pythonpath:
os.environ['PYTHONPATH'] = ':'.join((module_path, pythonpath))
else:
os.environ['PYTHONPATH'] = module_path
p = subprocess.Popen(['%(interpreter)s', '-m', 'ansible.module_exec.%(ansible_module)s.__main__'], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
(stdout, stderr) = p.communicate(json_params)
if not isinstance(stderr, (bytes, unicode)):
stderr = stderr.read()
if not isinstance(stdout, (bytes, unicode)):
stdout = stdout.read()
sys.stderr.write(stderr)
sys.stdout.write(stdout)
return p.returncode
def debug(command, zipped_mod, json_params):
# The code here normally doesn't run. It's only used for debugging on the # 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 # 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 # to save the module file remotely. Login to the remote machine and use
@ -107,7 +133,7 @@ def debug(command, zipped_mod):
# files. Edit the source files to instrument the code or experiment with # files. Edit the source files to instrument the code or experiment with
# different values. Then use /path/to/module execute to run the extracted # different values. Then use /path/to/module execute to run the extracted
# files you've edited instead of the actual zipped module. # files you've edited instead of the actual zipped module.
#
# Okay to use __file__ here because we're running from a kept file # Okay to use __file__ here because we're running from a kept file
basedir = os.path.dirname(__file__) basedir = os.path.dirname(__file__)
if command == 'explode': if command == 'explode':
@ -120,6 +146,7 @@ def debug(command, zipped_mod):
for filename in z.namelist(): for filename in z.namelist():
if filename.startswith('/'): if filename.startswith('/'):
raise Exception('Something wrong with this module zip file: should not contain absolute paths') raise Exception('Something wrong with this module zip file: should not contain absolute paths')
dest_filename = os.path.join(basedir, filename) dest_filename = os.path.join(basedir, filename)
if dest_filename.endswith(os.path.sep) and not os.path.exists(dest_filename): if dest_filename.endswith(os.path.sep) and not os.path.exists(dest_filename):
os.makedirs(dest_filename) os.makedirs(dest_filename)
@ -130,26 +157,17 @@ def debug(command, zipped_mod):
f = open(dest_filename, 'w') f = open(dest_filename, 'w')
f.write(z.read(filename)) f.write(z.read(filename))
f.close() f.close()
print('Module expanded into:') print('Module expanded into:')
print('%%s' %% os.path.join(basedir, 'ansible')) print('%%s' %% os.path.join(basedir, 'ansible'))
exitcode = 0
elif command == 'execute': elif command == 'execute':
# Execute the exploded code instead of executing the module from the # Execute the exploded code instead of executing the module from the
# embedded ZIPDATA. This allows people to easily run their modified # embedded ZIPDATA. This allows people to easily run their modified
# code on the remote machine to see how changes will affect it. # code on the remote machine to see how changes will affect it.
pythonpath = os.environ.get('PYTHONPATH') exitcode = invoke_module(basedir, json_params)
if pythonpath:
os.environ['PYTHONPATH'] = ':'.join((basedir, pythonpath))
else:
os.environ['PYTHONPATH'] = basedir
p = subprocess.Popen(['%(interpreter)s', '-m', 'ansible.module_exec.%(ansible_module)s.__main__'], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate()
if not isinstance(stderr, (bytes, unicode)):
stderr = stderr.read()
if not isinstance(stdout, (bytes, unicode)):
stdout = stdout.read()
sys.stderr.write(stderr)
sys.stdout.write(stdout)
sys.exit(p.returncode)
elif command == 'excommunicate': elif command == 'excommunicate':
# This attempts to run the module in-process (by importing a main # This attempts to run the module in-process (by importing a main
# function and then calling it). It is not the way ansible generally # function and then calling it). It is not the way ansible generally
@ -159,41 +177,41 @@ def debug(command, zipped_mod):
# when using this that are only artifacts of how we're invoking here, # when using this that are only artifacts of how we're invoking here,
# not actual bugs (as they don't affect the real way that we invoke # not actual bugs (as they don't affect the real way that we invoke
# ansible modules) # ansible modules)
sys.stdin = IOStream(json_params)
sys.path.insert(0, basedir) sys.path.insert(0, basedir)
from ansible.module_exec.%(ansible_module)s.__main__ import main from ansible.module_exec.%(ansible_module)s.__main__ import main
main() main()
print('WARNING: Module returned to wrapper instead of exiting')
os.environ['ANSIBLE_MODULE_ARGS'] = %(args)s sys.exit(1)
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: else:
pythonpath = os.environ.get('PYTHONPATH') print('WARNING: Unknown debug command. Doing nothing.')
if pythonpath: exitcode = 0
os.environ['PYTHONPATH'] = ':'.join((temp_path, pythonpath))
else: return exitcode
os.environ['PYTHONPATH'] = temp_path
p = subprocess.Popen(['%(interpreter)s', '-m', 'ansible.module_exec.%(ansible_module)s.__main__'], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if __name__ == '__main__':
(stdout, stderr) = p.communicate() ZIPLOADER_PARAMS = %(params)s
if not isinstance(stderr, (bytes, unicode)):
stderr = stderr.read()
if not isinstance(stdout, (bytes, unicode)):
stdout = stdout.read()
sys.stderr.write(stderr)
sys.stdout.write(stdout)
sys.exit(p.returncode)
finally:
try: try:
temp_fd, temp_path = tempfile.mkstemp(prefix='ansible_')
os.write(temp_fd, base64.b64decode(ZIPDATA))
os.close(temp_fd) os.close(temp_fd)
os.remove(temp_path) if len(sys.argv) == 2:
except NameError: exitcode = debug(sys.argv[1], temp_path, ZIPLOADER_PARAMS)
# mkstemp failed else:
pass exitcode = invoke_module(temp_path, ZIPLOADER_PARAMS)
finally:
try:
try:
os.close(temp_fd)
except OSError:
# Already closed
pass
os.remove(temp_path)
except NameError:
# mkstemp failed
pass
sys.exit(exitcode)
''' '''
class ModuleDepFinder(ast.NodeVisitor): class ModuleDepFinder(ast.NodeVisitor):
@ -336,19 +354,21 @@ def _find_snippet_imports(module_name, module_data, module_path, module_args, ta
if module_style in ('old', 'non_native_want_json'): if module_style in ('old', 'non_native_want_json'):
return module_data, module_style, shebang return module_data, module_style, shebang
module_args_json = to_bytes(json.dumps(module_args))
output = BytesIO() output = BytesIO()
snippet_names = set() snippet_names = set()
if module_substyle == 'python': if module_substyle == 'python':
# ziploader for new-style python classes # ziploader for new-style python classes
python_repred_args = to_bytes(repr(module_args_json))
constants = dict( constants = dict(
SELINUX_SPECIAL_FS=C.DEFAULT_SELINUX_SPECIAL_FS, SELINUX_SPECIAL_FS=C.DEFAULT_SELINUX_SPECIAL_FS,
SYSLOG_FACILITY=_get_facility(task_vars), SYSLOG_FACILITY=_get_facility(task_vars),
) )
python_repred_constants = to_bytes(repr(json.dumps(constants)), errors='strict') params = dict(ANSIBLE_MODULE_ARGS=module_args,
ANSIBLE_MODULE_CONSTANTS=constants,
)
#python_repred_args = to_bytes(repr(module_args_json))
#python_repred_constants = to_bytes(repr(json.dumps(constants)), errors='strict')
python_repred_params = to_bytes(repr(json.dumps(params)), errors='strict')
try: try:
compression_method = getattr(zipfile, module_compression) compression_method = getattr(zipfile, module_compression)
@ -411,8 +431,9 @@ def _find_snippet_imports(module_name, module_data, module_path, module_args, ta
output.write(to_bytes(STRIPPED_ZIPLOADER_TEMPLATE % dict( output.write(to_bytes(STRIPPED_ZIPLOADER_TEMPLATE % dict(
zipdata=zipdata, zipdata=zipdata,
ansible_module=module_name, ansible_module=module_name,
args=python_repred_args, #args=python_repred_args,
constants=python_repred_constants, #constants=python_repred_constants,
params=python_repred_params,
shebang=shebang, shebang=shebang,
interpreter=interpreter, interpreter=interpreter,
coding=ENCODING_STRING, coding=ENCODING_STRING,
@ -437,6 +458,8 @@ def _find_snippet_imports(module_name, module_data, module_path, module_args, ta
continue continue
output.write(line + b'\n') output.write(line + b'\n')
module_data = output.getvalue() module_data = output.getvalue()
module_args_json = to_bytes(json.dumps(module_args))
module_data = module_data.replace(REPLACER_JSONARGS, module_args_json) module_data = module_data.replace(REPLACER_JSONARGS, module_args_json)
# Sanity check from 1.x days. This is currently useless as we only # Sanity check from 1.x days. This is currently useless as we only
@ -447,11 +470,14 @@ def _find_snippet_imports(module_name, module_data, module_path, module_args, ta
raise AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path) raise AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path)
elif module_substyle == 'jsonargs': elif module_substyle == 'jsonargs':
module_args_json = to_bytes(json.dumps(module_args))
# these strings could be included in a third-party module but # these strings could be included in a third-party module but
# officially they were included in the 'basic' snippet for new-style # officially they were included in the 'basic' snippet for new-style
# python modules (which has been replaced with something else in # python modules (which has been replaced with something else in
# ziploader) If we remove them from jsonargs-style module replacer # ziploader) If we remove them from jsonargs-style module replacer
# then we can remove them everywhere. # then we can remove them everywhere.
python_repred_args = to_bytes(repr(module_args_json))
module_data = module_data.replace(REPLACER_VERSION, to_bytes(repr(__version__))) 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_COMPLEX, python_repred_args)
module_data = module_data.replace(REPLACER_SELINUX, to_bytes(','.join(C.DEFAULT_SELINUX_SPECIAL_FS))) module_data = module_data.replace(REPLACER_SELINUX, to_bytes(','.join(C.DEFAULT_SELINUX_SPECIAL_FS)))

View file

@ -223,23 +223,6 @@ from ansible import __version__
# Backwards compat. New code should just import and use __version__ # Backwards compat. New code should just import and use __version__
ANSIBLE_VERSION = __version__ ANSIBLE_VERSION = __version__
try:
# MODULE_COMPLEX_ARGS is an old name kept for backwards compat
MODULE_COMPLEX_ARGS = os.environ.pop('ANSIBLE_MODULE_ARGS')
except KeyError:
# This file might be used for its utility functions. So don't fail if
# running outside of a module environment (will fail in _load_params()
# instead)
MODULE_COMPLEX_ARGS = None
try:
# ARGS are for parameters given in the playbook. Constants are for things
# that ansible needs to configure controller side but are passed to all
# modules.
MODULE_CONSTANTS = os.environ.pop('ANSIBLE_MODULE_CONSTANTS')
except KeyError:
MODULE_CONSTANTS = None
FILE_COMMON_ARGUMENTS=dict( FILE_COMMON_ARGUMENTS=dict(
src = dict(), src = dict(),
mode = dict(type='raw'), mode = dict(type='raw'),
@ -560,7 +543,6 @@ class AnsibleModule(object):
if k not in self.argument_spec: if k not in self.argument_spec:
self.argument_spec[k] = v self.argument_spec[k] = v
self._load_constants()
self._load_params() self._load_params()
self._set_fallbacks() self._set_fallbacks()
@ -1452,32 +1434,47 @@ class AnsibleModule(object):
continue continue
def _load_params(self): def _load_params(self):
''' read the input and set the params attribute''' ''' read the input and set the params attribute. Sets the constants as well.'''
if MODULE_COMPLEX_ARGS is None: buffer = sys.stdin.read()
try:
params = json.loads(buffer)
except ValueError:
# This helper used too early for fail_json to work. # This helper used too early for fail_json to work.
print('{"msg": "Error: ANSIBLE_MODULE_ARGS not found in environment. Unable to figure out what parameters were passed", "failed": true}') print('{"msg": "Error: Module unable to decode valid JSON on stdin. Unable to figure out what parameters were passed", "failed": true}')
sys.exit(1) sys.exit(1)
params = json_dict_unicode_to_bytes(json.loads(MODULE_COMPLEX_ARGS)) try:
if params is None: self.params = params['ANSIBLE_MODULE_ARGS']
params = dict() self.constants = params['ANSIBLE_MODULE_CONSTANTS']
self.params = params except KeyError:
def _load_constants(self):
''' read the input and set the constants attribute'''
if MODULE_CONSTANTS is None:
# This helper used too early for fail_json to work. # This helper used too early for fail_json to work.
print('{"msg": "Error: ANSIBLE_MODULE_CONSTANTS not found in environment. Unable to figure out what constants were passed", "failed": true}') print('{"msg": "Error: Module unable to locate ANSIBLE_MODULE_ARGS and ANSIBLE_MODULE_CONSTANTS in json data from stdin. Unable to figure out what parameters were passed", "failed": true}')
sys.exit(1) sys.exit(1)
# Make constants into "native string" # import select
if sys.version_info >= (3,): # buffer = ''
constants = json_dict_bytes_to_unicode(json.loads(MODULE_CONSTANTS)) # while True:
else: # input_list = select.select([sys.stdin], [], [], 5.0)[0]
constants = json_dict_unicode_to_bytes(json.loads(MODULE_CONSTANTS)) # if sys.stdin not in input_list:
if constants is None: # # This helper used too early for fail_json to work.
constants = dict() # print('{"msg": "Error: Module unable to read arguments from stdin. Unable to figure out what parameters were passed", "failed": true}')
self.constants = constants # sys.exit(1)
# buffer += sys.stdin.read()
# if json.loads(buffer):
#
# for line in sys.stdin:
# if line is None:
# print('s')
# data = sys.stdin.read()
# if MODULE_COMPLEX_ARGS is None:
# # This helper used too early for fail_json to work.
# print('{"msg": "Error: ANSIBLE_MODULE_ARGS not found in environment. Unable to figure out what parameters were passed", "failed": true}')
# sys.exit(1)
#
# params = json_dict_unicode_to_bytes(json.loads(data))
# if params is None:
# params = dict()
# self.params = params
def _log_to_syslog(self, msg): def _log_to_syslog(self, msg):
if HAS_SYSLOG: if HAS_SYSLOG: