mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-07 22:02:50 +00:00
Making the switch to v2
This commit is contained in:
334
lib/ansible/plugins/__init__.py
Normal file
334
lib/ansible/plugins/__init__.py
Normal file
@@ -0,0 +1,334 @@
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> and others
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import glob
|
||||
import imp
|
||||
import inspect
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.utils.display import Display
|
||||
from ansible import errors
|
||||
|
||||
MODULE_CACHE = {}
|
||||
PATH_CACHE = {}
|
||||
PLUGIN_PATH_CACHE = {}
|
||||
_basedirs = []
|
||||
|
||||
def push_basedir(basedir):
|
||||
# avoid pushing the same absolute dir more than once
|
||||
basedir = os.path.realpath(basedir)
|
||||
if basedir not in _basedirs:
|
||||
_basedirs.insert(0, basedir)
|
||||
|
||||
def get_all_plugin_loaders():
|
||||
return [(name, obj) for (name, obj) in inspect.getmembers(sys.modules[__name__]) if isinstance(obj, PluginLoader)]
|
||||
|
||||
class PluginLoader:
|
||||
|
||||
'''
|
||||
PluginLoader loads plugins from the configured plugin directories.
|
||||
|
||||
It searches for plugins by iterating through the combined list of
|
||||
play basedirs, configured paths, and the python path.
|
||||
The first match is used.
|
||||
'''
|
||||
|
||||
def __init__(self, class_name, package, config, subdir, aliases={}):
|
||||
|
||||
self.class_name = class_name
|
||||
self.package = package
|
||||
self.config = config
|
||||
self.subdir = subdir
|
||||
self.aliases = aliases
|
||||
|
||||
if not class_name in MODULE_CACHE:
|
||||
MODULE_CACHE[class_name] = {}
|
||||
if not class_name in PATH_CACHE:
|
||||
PATH_CACHE[class_name] = None
|
||||
if not class_name in PLUGIN_PATH_CACHE:
|
||||
PLUGIN_PATH_CACHE[class_name] = {}
|
||||
|
||||
self._module_cache = MODULE_CACHE[class_name]
|
||||
self._paths = PATH_CACHE[class_name]
|
||||
self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]
|
||||
|
||||
self._extra_dirs = []
|
||||
self._searched_paths = set()
|
||||
|
||||
def print_paths(self):
|
||||
''' Returns a string suitable for printing of the search path '''
|
||||
|
||||
# Uses a list to get the order right
|
||||
ret = []
|
||||
for i in self._get_paths():
|
||||
if i not in ret:
|
||||
ret.append(i)
|
||||
return os.pathsep.join(ret)
|
||||
|
||||
def _all_directories(self, dir):
|
||||
results = []
|
||||
results.append(dir)
|
||||
for root, subdirs, files in os.walk(dir):
|
||||
if '__init__.py' in files:
|
||||
for x in subdirs:
|
||||
results.append(os.path.join(root,x))
|
||||
return results
|
||||
|
||||
def _get_package_paths(self):
|
||||
''' Gets the path of a Python package '''
|
||||
|
||||
paths = []
|
||||
if not self.package:
|
||||
return []
|
||||
if not hasattr(self, 'package_path'):
|
||||
m = __import__(self.package)
|
||||
parts = self.package.split('.')[1:]
|
||||
self.package_path = os.path.join(os.path.dirname(m.__file__), *parts)
|
||||
paths.extend(self._all_directories(self.package_path))
|
||||
return paths
|
||||
|
||||
def _get_paths(self):
|
||||
''' Return a list of paths to search for plugins in '''
|
||||
|
||||
if self._paths is not None:
|
||||
return self._paths
|
||||
|
||||
ret = self._extra_dirs[:]
|
||||
for basedir in _basedirs:
|
||||
fullpath = os.path.realpath(os.path.join(basedir, self.subdir))
|
||||
if os.path.isdir(fullpath):
|
||||
files = glob.glob("%s/*" % fullpath)
|
||||
|
||||
# allow directories to be two levels deep
|
||||
files2 = glob.glob("%s/*/*" % fullpath)
|
||||
|
||||
if files2 is not None:
|
||||
files.extend(files2)
|
||||
|
||||
for file in files:
|
||||
if os.path.isdir(file) and file not in ret:
|
||||
ret.append(file)
|
||||
if fullpath not in ret:
|
||||
ret.append(fullpath)
|
||||
|
||||
# look in any configured plugin paths, allow one level deep for subcategories
|
||||
if self.config is not None:
|
||||
configured_paths = self.config.split(os.pathsep)
|
||||
for path in configured_paths:
|
||||
path = os.path.realpath(os.path.expanduser(path))
|
||||
contents = glob.glob("%s/*" % path) + glob.glob("%s/*/*" % path)
|
||||
for c in contents:
|
||||
if os.path.isdir(c) and c not in ret:
|
||||
ret.append(c)
|
||||
if path not in ret:
|
||||
ret.append(path)
|
||||
|
||||
# look for any plugins installed in the package subtree
|
||||
ret.extend(self._get_package_paths())
|
||||
|
||||
# cache and return the result
|
||||
self._paths = ret
|
||||
return ret
|
||||
|
||||
|
||||
def add_directory(self, directory, with_subdir=False):
|
||||
''' Adds an additional directory to the search path '''
|
||||
|
||||
directory = os.path.realpath(directory)
|
||||
|
||||
if directory is not None:
|
||||
if with_subdir:
|
||||
directory = os.path.join(directory, self.subdir)
|
||||
if directory not in self._extra_dirs:
|
||||
# append the directory and invalidate the path cache
|
||||
self._extra_dirs.append(directory)
|
||||
self._paths = None
|
||||
|
||||
def find_plugin(self, name, suffixes=None):
|
||||
''' Find a plugin named name '''
|
||||
|
||||
if not suffixes:
|
||||
if self.class_name:
|
||||
suffixes = ['.py']
|
||||
else:
|
||||
suffixes = ['.py', '']
|
||||
|
||||
potential_names = frozenset('%s%s' % (name, s) for s in suffixes)
|
||||
for full_name in potential_names:
|
||||
if full_name in self._plugin_path_cache:
|
||||
return self._plugin_path_cache[full_name]
|
||||
|
||||
found = None
|
||||
for path in [p for p in self._get_paths() if p not in self._searched_paths]:
|
||||
if os.path.isdir(path):
|
||||
try:
|
||||
full_paths = (os.path.join(path, f) for f in os.listdir(path))
|
||||
except OSError as e:
|
||||
d = Display()
|
||||
d.warning("Error accessing plugin paths: %s" % str(e))
|
||||
for full_path in (f for f in full_paths if os.path.isfile(f)):
|
||||
for suffix in suffixes:
|
||||
if full_path.endswith(suffix):
|
||||
full_name = os.path.basename(full_path)
|
||||
break
|
||||
else: # Yes, this is a for-else: http://bit.ly/1ElPkyg
|
||||
continue
|
||||
|
||||
if full_name not in self._plugin_path_cache:
|
||||
self._plugin_path_cache[full_name] = full_path
|
||||
|
||||
self._searched_paths.add(path)
|
||||
for full_name in potential_names:
|
||||
if full_name in self._plugin_path_cache:
|
||||
return self._plugin_path_cache[full_name]
|
||||
|
||||
# if nothing is found, try finding alias/deprecated
|
||||
if not name.startswith('_'):
|
||||
for alias_name in ('_%s' % n for n in potential_names):
|
||||
# We've already cached all the paths at this point
|
||||
if alias_name in self._plugin_path_cache:
|
||||
return self._plugin_path_cache[alias_name]
|
||||
|
||||
return None
|
||||
|
||||
def has_plugin(self, name):
|
||||
''' Checks if a plugin named name exists '''
|
||||
|
||||
return self.find_plugin(name) is not None
|
||||
|
||||
__contains__ = has_plugin
|
||||
|
||||
def get(self, name, *args, **kwargs):
|
||||
''' instantiates a plugin of the given name using arguments '''
|
||||
|
||||
if name in self.aliases:
|
||||
name = self.aliases[name]
|
||||
path = self.find_plugin(name)
|
||||
if path is None:
|
||||
return None
|
||||
elif kwargs.get('class_only', False):
|
||||
return getattr(self._module_cache[path], self.class_name)
|
||||
|
||||
if path not in self._module_cache:
|
||||
self._module_cache[path] = imp.load_source('.'.join([self.package, name]), path)
|
||||
return getattr(self._module_cache[path], self.class_name)(*args, **kwargs)
|
||||
|
||||
def all(self, *args, **kwargs):
|
||||
''' instantiates all plugins with the same arguments '''
|
||||
|
||||
for i in self._get_paths():
|
||||
matches = glob.glob(os.path.join(i, "*.py"))
|
||||
matches.sort()
|
||||
for path in matches:
|
||||
name, ext = os.path.splitext(os.path.basename(path))
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if path not in self._module_cache:
|
||||
self._module_cache[path] = imp.load_source('.'.join([self.package, name]), path)
|
||||
if kwargs.get('class_only', False):
|
||||
obj = getattr(self._module_cache[path], self.class_name)
|
||||
else:
|
||||
obj = getattr(self._module_cache[path], self.class_name)(*args, **kwargs)
|
||||
# set extra info on the module, in case we want it later
|
||||
setattr(obj, '_original_path', path)
|
||||
yield obj
|
||||
|
||||
action_loader = PluginLoader(
|
||||
'ActionModule',
|
||||
'ansible.plugins.action',
|
||||
C.DEFAULT_ACTION_PLUGIN_PATH,
|
||||
'action_plugins'
|
||||
)
|
||||
|
||||
cache_loader = PluginLoader(
|
||||
'CacheModule',
|
||||
'ansible.plugins.cache',
|
||||
C.DEFAULT_CACHE_PLUGIN_PATH,
|
||||
'cache_plugins'
|
||||
)
|
||||
|
||||
callback_loader = PluginLoader(
|
||||
'CallbackModule',
|
||||
'ansible.plugins.callback',
|
||||
C.DEFAULT_CALLBACK_PLUGIN_PATH,
|
||||
'callback_plugins'
|
||||
)
|
||||
|
||||
connection_loader = PluginLoader(
|
||||
'Connection',
|
||||
'ansible.plugins.connections',
|
||||
C.DEFAULT_CONNECTION_PLUGIN_PATH,
|
||||
'connection_plugins',
|
||||
aliases={'paramiko': 'paramiko_ssh'}
|
||||
)
|
||||
|
||||
shell_loader = PluginLoader(
|
||||
'ShellModule',
|
||||
'ansible.plugins.shell',
|
||||
'shell_plugins',
|
||||
'shell_plugins',
|
||||
)
|
||||
|
||||
module_loader = PluginLoader(
|
||||
'',
|
||||
'ansible.modules',
|
||||
C.DEFAULT_MODULE_PATH,
|
||||
'library'
|
||||
)
|
||||
|
||||
lookup_loader = PluginLoader(
|
||||
'LookupModule',
|
||||
'ansible.plugins.lookup',
|
||||
C.DEFAULT_LOOKUP_PLUGIN_PATH,
|
||||
'lookup_plugins'
|
||||
)
|
||||
|
||||
vars_loader = PluginLoader(
|
||||
'VarsModule',
|
||||
'ansible.plugins.vars',
|
||||
C.DEFAULT_VARS_PLUGIN_PATH,
|
||||
'vars_plugins'
|
||||
)
|
||||
|
||||
filter_loader = PluginLoader(
|
||||
'FilterModule',
|
||||
'ansible.plugins.filter',
|
||||
C.DEFAULT_FILTER_PLUGIN_PATH,
|
||||
'filter_plugins'
|
||||
)
|
||||
|
||||
fragment_loader = PluginLoader(
|
||||
'ModuleDocFragment',
|
||||
'ansible.utils.module_docs_fragments',
|
||||
os.path.join(os.path.dirname(__file__), 'module_docs_fragments'),
|
||||
'',
|
||||
)
|
||||
|
||||
strategy_loader = PluginLoader(
|
||||
'StrategyModule',
|
||||
'ansible.plugins.strategies',
|
||||
None,
|
||||
'strategy_plugins',
|
||||
)
|
||||
471
lib/ansible/plugins/action/__init__.py
Normal file
471
lib/ansible/plugins/action/__init__.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from six.moves import StringIO
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys # FIXME: probably not needed
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.executor.module_common import modify_module
|
||||
from ansible.parsing.utils.jsonify import jsonify
|
||||
from ansible.plugins import shell_loader
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
class ActionBase:
|
||||
|
||||
'''
|
||||
This class is the base class for all action plugins, and defines
|
||||
code common to all actions. The base class handles the connection
|
||||
by putting/getting files and executing commands based on the current
|
||||
action in use.
|
||||
'''
|
||||
|
||||
def __init__(self, task, connection, connection_info, loader, shared_loader_obj):
|
||||
self._task = task
|
||||
self._connection = connection
|
||||
self._connection_info = connection_info
|
||||
self._loader = loader
|
||||
self._shared_loader_obj = shared_loader_obj
|
||||
self._shell = self.get_shell()
|
||||
|
||||
self._supports_check_mode = True
|
||||
|
||||
def get_shell(self):
|
||||
|
||||
if hasattr(self._connection, '_shell'):
|
||||
shell_plugin = getattr(self._connection, '_shell', '')
|
||||
else:
|
||||
shell_plugin = shell_loader.get(os.path.basename(C.DEFAULT_EXECUTABLE))
|
||||
if shell_plugin is None:
|
||||
shell_plugin = shell_loader.get('sh')
|
||||
|
||||
return shell_plugin
|
||||
|
||||
def _configure_module(self, module_name, module_args):
|
||||
'''
|
||||
Handles the loading and templating of the module code through the
|
||||
modify_module() function.
|
||||
'''
|
||||
|
||||
# Search module path(s) for named module.
|
||||
module_suffixes = getattr(self._connection, 'default_suffixes', None)
|
||||
module_path = self._shared_loader_obj.module_loader.find_plugin(module_name, module_suffixes)
|
||||
if module_path is None:
|
||||
module_path2 = self._shared_loader_obj.module_loader.find_plugin('ping', module_suffixes)
|
||||
if module_path2 is not None:
|
||||
raise AnsibleError("The module %s was not found in configured module paths" % (module_name))
|
||||
else:
|
||||
raise AnsibleError("The module %s was not found in configured module paths. " \
|
||||
"Additionally, core modules are missing. If this is a checkout, " \
|
||||
"run 'git submodule update --init --recursive' to correct this problem." % (module_name))
|
||||
|
||||
# insert shared code and arguments into the module
|
||||
(module_data, module_style, module_shebang) = modify_module(module_path, module_args)
|
||||
|
||||
return (module_style, module_shebang, module_data)
|
||||
|
||||
def _compute_environment_string(self):
|
||||
'''
|
||||
Builds the environment string to be used when executing the remote task.
|
||||
'''
|
||||
|
||||
enviro = {}
|
||||
|
||||
# FIXME: not sure where this comes from, probably task but maybe also the play?
|
||||
#if self.environment:
|
||||
# enviro = template.template(self.basedir, self.environment, inject, convert_bare=True)
|
||||
# enviro = utils.safe_eval(enviro)
|
||||
# if type(enviro) != dict:
|
||||
# raise errors.AnsibleError("environment must be a dictionary, received %s" % enviro)
|
||||
|
||||
return self._shell.env_prefix(**enviro)
|
||||
|
||||
def _early_needs_tmp_path(self):
|
||||
'''
|
||||
Determines if a temp path should be created before the action is executed.
|
||||
'''
|
||||
|
||||
# FIXME: modified from original, needs testing? Since this is now inside
|
||||
# the action plugin, it should make it just this simple
|
||||
return getattr(self, 'TRANSFERS_FILES', False)
|
||||
|
||||
def _late_needs_tmp_path(self, tmp, module_style):
|
||||
'''
|
||||
Determines if a temp path is required after some early actions have already taken place.
|
||||
'''
|
||||
if tmp and "tmp" in tmp:
|
||||
# tmp has already been created
|
||||
return False
|
||||
if not self._connection.__class__.has_pipelining or not C.ANSIBLE_SSH_PIPELINING or C.DEFAULT_KEEP_REMOTE_FILES or self._connection_info.become:
|
||||
# tmp is necessary to store module source code
|
||||
return True
|
||||
if not self._connection.__class__.has_pipelining:
|
||||
# tmp is necessary to store the module source code
|
||||
# or we want to keep the files on the target system
|
||||
return True
|
||||
if module_style != "new":
|
||||
# even when conn has pipelining, old style modules need tmp to store arguments
|
||||
return True
|
||||
return False
|
||||
|
||||
# FIXME: return a datastructure in this function instead of raising errors -
|
||||
# the new executor pipeline handles it much better that way
|
||||
def _make_tmp_path(self):
|
||||
'''
|
||||
Create and return a temporary path on a remote box.
|
||||
'''
|
||||
|
||||
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
|
||||
use_system_tmp = False
|
||||
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
use_system_tmp = True
|
||||
|
||||
tmp_mode = None
|
||||
if self._connection_info.remote_user != 'root' or self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
tmp_mode = 'a+rx'
|
||||
|
||||
cmd = self._shell.mkdtemp(basefile, use_system_tmp, tmp_mode)
|
||||
debug("executing _low_level_execute_command to create the tmp path")
|
||||
result = self._low_level_execute_command(cmd, None, sudoable=False)
|
||||
debug("done with creation of tmp path")
|
||||
|
||||
# error handling on this seems a little aggressive?
|
||||
if result['rc'] != 0:
|
||||
if result['rc'] == 5:
|
||||
output = 'Authentication failure.'
|
||||
elif result['rc'] == 255 and self._connection.transport in ('ssh',):
|
||||
# FIXME: more utils.VERBOSITY
|
||||
#if utils.VERBOSITY > 3:
|
||||
# output = 'SSH encountered an unknown error. The output was:\n%s' % (result['stdout']+result['stderr'])
|
||||
#else:
|
||||
# output = 'SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue'
|
||||
output = 'SSH encountered an unknown error. The output was:\n%s' % (result['stdout']+result['stderr'])
|
||||
elif 'No space left on device' in result['stderr']:
|
||||
output = result['stderr']
|
||||
else:
|
||||
output = 'Authentication or permission failure. In some cases, you may have been able to authenticate and did not have permissions on the remote directory. Consider changing the remote temp path in ansible.cfg to a path rooted in "/tmp". Failed command was: %s, exited with result %d' % (cmd, result['rc'])
|
||||
if 'stdout' in result and result['stdout'] != '':
|
||||
output = output + ": %s" % result['stdout']
|
||||
raise AnsibleError(output)
|
||||
|
||||
# FIXME: do we still need to do this?
|
||||
#rc = self._shell.join_path(utils.last_non_blank_line(result['stdout']).strip(), '')
|
||||
rc = self._shell.join_path(result['stdout'].strip(), '').splitlines()[-1]
|
||||
|
||||
# Catch failure conditions, files should never be
|
||||
# written to locations in /.
|
||||
if rc == '/':
|
||||
raise AnsibleError('failed to resolve remote temporary directory from %s: `%s` returned empty string' % (basefile, cmd))
|
||||
|
||||
return rc
|
||||
|
||||
def _remove_tmp_path(self, tmp_path):
|
||||
'''Remove a temporary path we created. '''
|
||||
|
||||
if tmp_path and "-tmp-" in tmp_path:
|
||||
cmd = self._shell.remove(tmp_path, recurse=True)
|
||||
# If we have gotten here we have a working ssh configuration.
|
||||
# If ssh breaks we could leave tmp directories out on the remote system.
|
||||
debug("calling _low_level_execute_command to remove the tmp path")
|
||||
self._low_level_execute_command(cmd, None, sudoable=False)
|
||||
debug("done removing the tmp path")
|
||||
|
||||
def _transfer_data(self, remote_path, data):
|
||||
'''
|
||||
Copies the module data out to the temporary module path.
|
||||
'''
|
||||
|
||||
if type(data) == dict:
|
||||
data = jsonify(data)
|
||||
|
||||
afd, afile = tempfile.mkstemp()
|
||||
afo = os.fdopen(afd, 'w')
|
||||
try:
|
||||
# FIXME: is this still necessary?
|
||||
#if not isinstance(data, unicode):
|
||||
# #ensure the data is valid UTF-8
|
||||
# data = data.decode('utf-8')
|
||||
#else:
|
||||
# data = data.encode('utf-8')
|
||||
afo.write(data)
|
||||
except Exception as e:
|
||||
#raise AnsibleError("failure encoding into utf-8: %s" % str(e))
|
||||
raise AnsibleError("failure writing module data to temporary file for transfer: %s" % str(e))
|
||||
|
||||
afo.flush()
|
||||
afo.close()
|
||||
|
||||
try:
|
||||
self._connection.put_file(afile, remote_path)
|
||||
finally:
|
||||
os.unlink(afile)
|
||||
|
||||
return remote_path
|
||||
|
||||
def _remote_chmod(self, tmp, mode, path, sudoable=False):
|
||||
'''
|
||||
Issue a remote chmod command
|
||||
'''
|
||||
|
||||
cmd = self._shell.chmod(mode, path)
|
||||
debug("calling _low_level_execute_command to chmod the remote path")
|
||||
res = self._low_level_execute_command(cmd, tmp, sudoable=sudoable)
|
||||
debug("done with chmod call")
|
||||
return res
|
||||
|
||||
def _remote_checksum(self, tmp, path):
|
||||
'''
|
||||
Takes a remote checksum and returns 1 if no file
|
||||
'''
|
||||
|
||||
# FIXME: figure out how this will work, probably pulled from the
|
||||
# variable manager data
|
||||
#python_interp = inject['hostvars'][inject['inventory_hostname']].get('ansible_python_interpreter', 'python')
|
||||
python_interp = 'python'
|
||||
cmd = self._shell.checksum(path, python_interp)
|
||||
debug("calling _low_level_execute_command to get the remote checksum")
|
||||
data = self._low_level_execute_command(cmd, tmp, sudoable=True)
|
||||
debug("done getting the remote checksum")
|
||||
# FIXME: implement this function?
|
||||
#data2 = utils.last_non_blank_line(data['stdout'])
|
||||
try:
|
||||
data2 = data['stdout'].strip().splitlines()[-1]
|
||||
if data2 == '':
|
||||
# this may happen if the connection to the remote server
|
||||
# failed, so just return "INVALIDCHECKSUM" to avoid errors
|
||||
return "INVALIDCHECKSUM"
|
||||
else:
|
||||
return data2.split()[0]
|
||||
except IndexError:
|
||||
# FIXME: this should probably not print to sys.stderr, but should instead
|
||||
# fail in a more normal way?
|
||||
sys.stderr.write("warning: Calculating checksum failed unusually, please report this to the list so it can be fixed\n")
|
||||
sys.stderr.write("command: %s\n" % cmd)
|
||||
sys.stderr.write("----\n")
|
||||
sys.stderr.write("output: %s\n" % data)
|
||||
sys.stderr.write("----\n")
|
||||
# this will signal that it changed and allow things to keep going
|
||||
return "INVALIDCHECKSUM"
|
||||
|
||||
def _remote_expand_user(self, path, tmp):
|
||||
''' takes a remote path and performs tilde expansion on the remote host '''
|
||||
if not path.startswith('~'):
|
||||
return path
|
||||
|
||||
split_path = path.split(os.path.sep, 1)
|
||||
expand_path = split_path[0]
|
||||
if expand_path == '~':
|
||||
if self._connection_info.become and self._connection_info.become_user:
|
||||
expand_path = '~%s' % self._connection_info.become_user
|
||||
|
||||
cmd = self._shell.expand_user(expand_path)
|
||||
debug("calling _low_level_execute_command to expand the remote user path")
|
||||
data = self._low_level_execute_command(cmd, tmp, sudoable=False)
|
||||
debug("done expanding the remote user path")
|
||||
#initial_fragment = utils.last_non_blank_line(data['stdout'])
|
||||
initial_fragment = data['stdout'].strip().splitlines()[-1]
|
||||
|
||||
if not initial_fragment:
|
||||
# Something went wrong trying to expand the path remotely. Return
|
||||
# the original string
|
||||
return path
|
||||
|
||||
if len(split_path) > 1:
|
||||
return self._shell.join_path(initial_fragment, *split_path[1:])
|
||||
else:
|
||||
return initial_fragment
|
||||
|
||||
def _filter_leading_non_json_lines(self, data):
|
||||
'''
|
||||
Used to avoid random output from SSH at the top of JSON output, like messages from
|
||||
tcagetattr, or where dropbear spews MOTD on every single command (which is nuts).
|
||||
|
||||
need to filter anything which starts not with '{', '[', ', '=' or is an empty line.
|
||||
filter only leading lines since multiline JSON is valid.
|
||||
'''
|
||||
|
||||
filtered_lines = StringIO()
|
||||
stop_filtering = False
|
||||
for line in data.splitlines():
|
||||
if stop_filtering or line.startswith('{') or line.startswith('['):
|
||||
stop_filtering = True
|
||||
filtered_lines.write(line + '\n')
|
||||
return filtered_lines.getvalue()
|
||||
|
||||
def _execute_module(self, module_name=None, module_args=None, tmp=None, persist_files=False, delete_remote_tmp=True):
|
||||
'''
|
||||
Transfer and run a module along with its arguments.
|
||||
'''
|
||||
|
||||
# if a module name was not specified for this execution, use
|
||||
# the action from the task
|
||||
if module_name is None:
|
||||
module_name = self._task.action
|
||||
if module_args is None:
|
||||
module_args = self._task.args
|
||||
|
||||
# set check mode in the module arguments, if required
|
||||
if self._connection_info.check_mode and not self._task.always_run:
|
||||
if not self._supports_check_mode:
|
||||
raise AnsibleError("check mode is not supported for this operation")
|
||||
module_args['_ansible_check_mode'] = True
|
||||
|
||||
# set no log in the module arguments, if required
|
||||
if self._connection_info.no_log:
|
||||
module_args['_ansible_no_log'] = True
|
||||
|
||||
debug("in _execute_module (%s, %s)" % (module_name, module_args))
|
||||
|
||||
(module_style, shebang, module_data) = self._configure_module(module_name=module_name, module_args=module_args)
|
||||
if not shebang:
|
||||
raise AnsibleError("module is missing interpreter line")
|
||||
|
||||
# a remote tmp path may be necessary and not already created
|
||||
remote_module_path = None
|
||||
if not tmp and self._late_needs_tmp_path(tmp, module_style):
|
||||
tmp = self._make_tmp_path()
|
||||
remote_module_path = self._shell.join_path(tmp, module_name)
|
||||
|
||||
# FIXME: async stuff here?
|
||||
#if (module_style != 'new' or async_jid is not None or not self._connection._has_pipelining or not C.ANSIBLE_SSH_PIPELINING or C.DEFAULT_KEEP_REMOTE_FILES):
|
||||
if remote_module_path:
|
||||
debug("transferring module to remote")
|
||||
self._transfer_data(remote_module_path, module_data)
|
||||
debug("done transferring module to remote")
|
||||
|
||||
environment_string = self._compute_environment_string()
|
||||
|
||||
if tmp and "tmp" in tmp and self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
# deal with possible umask issues once sudo'ed to other user
|
||||
self._remote_chmod(tmp, 'a+r', remote_module_path)
|
||||
|
||||
cmd = ""
|
||||
in_data = None
|
||||
|
||||
# FIXME: all of the old-module style and async stuff has been removed from here, and
|
||||
# might need to be re-added (unless we decide to drop support for old-style modules
|
||||
# at this point and rework things to support non-python modules specifically)
|
||||
if self._connection.__class__.has_pipelining and C.ANSIBLE_SSH_PIPELINING and not C.DEFAULT_KEEP_REMOTE_FILES:
|
||||
in_data = module_data
|
||||
else:
|
||||
if remote_module_path:
|
||||
cmd = remote_module_path
|
||||
|
||||
rm_tmp = None
|
||||
if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
|
||||
if not self._connection_info.become or self._connection_info.become_user == 'root':
|
||||
# not sudoing or sudoing to root, so can cleanup files in the same step
|
||||
rm_tmp = tmp
|
||||
|
||||
cmd = self._shell.build_module_command(environment_string, shebang, cmd, rm_tmp)
|
||||
cmd = cmd.strip()
|
||||
|
||||
sudoable = True
|
||||
if module_name == "accelerate":
|
||||
# always run the accelerate module as the user
|
||||
# specified in the play, not the sudo_user
|
||||
sudoable = False
|
||||
|
||||
debug("calling _low_level_execute_command() for command %s" % cmd)
|
||||
res = self._low_level_execute_command(cmd, tmp, sudoable=sudoable, in_data=in_data)
|
||||
debug("_low_level_execute_command returned ok")
|
||||
|
||||
if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
# not sudoing to root, so maybe can't delete files as that other user
|
||||
# have to clean up temp files as original user in a second step
|
||||
cmd2 = self._shell.remove(tmp, recurse=True)
|
||||
self._low_level_execute_command(cmd2, tmp, sudoable=False)
|
||||
|
||||
try:
|
||||
data = json.loads(self._filter_leading_non_json_lines(res.get('stdout', '')))
|
||||
except ValueError:
|
||||
# not valid json, lets try to capture error
|
||||
data = dict(failed=True, parsed=False)
|
||||
if 'stderr' in res and res['stderr'].startswith('Traceback'):
|
||||
data['traceback'] = res['stderr']
|
||||
else:
|
||||
data['msg'] = res.get('stdout', '')
|
||||
if 'stderr' in res:
|
||||
data['msg'] += res['stderr']
|
||||
|
||||
# pre-split stdout into lines, if stdout is in the data and there
|
||||
# isn't already a stdout_lines value there
|
||||
if 'stdout' in data and 'stdout_lines' not in data:
|
||||
data['stdout_lines'] = data.get('stdout', '').splitlines()
|
||||
|
||||
# store the module invocation details back into the result
|
||||
data['invocation'] = dict(
|
||||
module_args = module_args,
|
||||
module_name = module_name,
|
||||
)
|
||||
|
||||
debug("done with _execute_module (%s, %s)" % (module_name, module_args))
|
||||
return data
|
||||
|
||||
def _low_level_execute_command(self, cmd, tmp, executable=None, sudoable=True, in_data=None):
|
||||
'''
|
||||
This is the function which executes the low level shell command, which
|
||||
may be commands to create/remove directories for temporary files, or to
|
||||
run the module code or python directly when pipelining.
|
||||
'''
|
||||
|
||||
debug("in _low_level_execute_command() (%s)" % (cmd,))
|
||||
if not cmd:
|
||||
# this can happen with powershell modules when there is no analog to a Windows command (like chmod)
|
||||
debug("no command, exiting _low_level_execute_command()")
|
||||
return dict(stdout='', stderr='')
|
||||
|
||||
if executable is None:
|
||||
executable = C.DEFAULT_EXECUTABLE
|
||||
|
||||
prompt = None
|
||||
success_key = None
|
||||
|
||||
if sudoable:
|
||||
cmd, prompt, success_key = self._connection_info.make_become_cmd(cmd, executable)
|
||||
|
||||
debug("executing the command %s through the connection" % cmd)
|
||||
rc, stdin, stdout, stderr = self._connection.exec_command(cmd, tmp, executable=executable, in_data=in_data)
|
||||
debug("command execution done")
|
||||
|
||||
if not isinstance(stdout, basestring):
|
||||
out = ''.join(stdout.readlines())
|
||||
else:
|
||||
out = stdout
|
||||
|
||||
if not isinstance(stderr, basestring):
|
||||
err = ''.join(stderr.readlines())
|
||||
else:
|
||||
err = stderr
|
||||
|
||||
debug("done with _low_level_execute_command() (%s)" % (cmd,))
|
||||
if rc is not None:
|
||||
return dict(rc=rc, stdout=out, stderr=err)
|
||||
else:
|
||||
return dict(stdout=out, stderr=err)
|
||||
62
lib/ansible/plugins/action/add_host.py
Normal file
62
lib/ansible/plugins/action/add_host.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Copyright 2012, Seth Vidal <skvidal@fedoraproject.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
''' Create inventory hosts and groups in the memory inventory'''
|
||||
|
||||
### We need to be able to modify the inventory
|
||||
BYPASS_HOST_LOOP = True
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
# FIXME: is this necessary in v2?
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
||||
|
||||
# Parse out any hostname:port patterns
|
||||
new_name = self._task.args.get('name', self._task.args.get('hostname', None))
|
||||
#vv("creating host via 'add_host': hostname=%s" % new_name)
|
||||
|
||||
if ":" in new_name:
|
||||
new_name, new_port = new_name.split(":")
|
||||
self._task.args['ansible_ssh_port'] = new_port
|
||||
|
||||
groups = self._task.args.get('groupname', self._task.args.get('groups', self._task.args.get('group', '')))
|
||||
# add it to the group if that was specified
|
||||
new_groups = []
|
||||
if groups:
|
||||
for group_name in groups.split(","):
|
||||
if group_name not in new_groups:
|
||||
new_groups.append(group_name.strip())
|
||||
|
||||
# Add any variables to the new_host
|
||||
host_vars = dict()
|
||||
for k in self._task.args.keys():
|
||||
if not k in [ 'name', 'hostname', 'groupname', 'groups' ]:
|
||||
host_vars[k] = self._task.args[k]
|
||||
|
||||
return dict(changed=True, add_host=dict(host_name=new_name, groups=new_groups, host_vars=host_vars))
|
||||
|
||||
|
||||
156
lib/ansible/plugins/action/assemble.py
Normal file
156
lib/ansible/plugins/action/assemble.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# (c) 2013-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Stephen Fromm <sfromm@gmail.com>
|
||||
# Brian Coca <briancoca+dev@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
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import pipes
|
||||
import shutil
|
||||
import tempfile
|
||||
import base64
|
||||
import re
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.boolean import boolean
|
||||
from ansible.utils.hashing import checksum_s
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
TRANSFERS_FILES = True
|
||||
|
||||
def _assemble_from_fragments(self, src_path, delimiter=None, compiled_regexp=None):
|
||||
''' assemble a file from a directory of fragments '''
|
||||
|
||||
tmpfd, temp_path = tempfile.mkstemp()
|
||||
tmp = os.fdopen(tmpfd,'w')
|
||||
delimit_me = False
|
||||
add_newline = False
|
||||
|
||||
for f in sorted(os.listdir(src_path)):
|
||||
if compiled_regexp and not compiled_regexp.search(f):
|
||||
continue
|
||||
fragment = "%s/%s" % (src_path, f)
|
||||
if not os.path.isfile(fragment):
|
||||
continue
|
||||
fragment_content = file(fragment).read()
|
||||
|
||||
# always put a newline between fragments if the previous fragment didn't end with a newline.
|
||||
if add_newline:
|
||||
tmp.write('\n')
|
||||
|
||||
# delimiters should only appear between fragments
|
||||
if delimit_me:
|
||||
if delimiter:
|
||||
# un-escape anything like newlines
|
||||
delimiter = delimiter.decode('unicode-escape')
|
||||
tmp.write(delimiter)
|
||||
# always make sure there's a newline after the
|
||||
# delimiter, so lines don't run together
|
||||
if delimiter[-1] != '\n':
|
||||
tmp.write('\n')
|
||||
|
||||
tmp.write(fragment_content)
|
||||
delimit_me = True
|
||||
if fragment_content.endswith('\n'):
|
||||
add_newline = False
|
||||
else:
|
||||
add_newline = True
|
||||
|
||||
tmp.close()
|
||||
return temp_path
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
src = self._task.args.get('src', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
delimiter = self._task.args.get('delimiter', None)
|
||||
remote_src = self._task.args.get('remote_src', 'yes')
|
||||
regexp = self._task.args.get('regexp', None)
|
||||
|
||||
if src is None or dest is None:
|
||||
return dict(failed=True, msg="src and dest are required")
|
||||
|
||||
if boolean(remote_src):
|
||||
return self._execute_module(tmp=tmp)
|
||||
elif self._task._role is not None:
|
||||
src = self._loader.path_dwim_relative(self._task._role._role_path, 'files', src)
|
||||
else:
|
||||
# the source is local, so expand it here
|
||||
src = self._loader.path_dwim(os.path.expanduser(src))
|
||||
|
||||
_re = None
|
||||
if regexp is not None:
|
||||
_re = re.compile(regexp)
|
||||
|
||||
# Does all work assembling the file
|
||||
path = self._assemble_from_fragments(src, delimiter, _re)
|
||||
|
||||
path_checksum = checksum_s(path)
|
||||
dest = self._remote_expand_user(dest, tmp)
|
||||
remote_checksum = self._remote_checksum(tmp, dest)
|
||||
|
||||
if path_checksum != remote_checksum:
|
||||
resultant = file(path).read()
|
||||
# FIXME: diff needs to be moved somewhere else
|
||||
#if self.runner.diff:
|
||||
# dest_result = self._execute_module(module_name='slurp', module_args=dict(path=dest), tmp=tmp, persist_files=True)
|
||||
# if 'content' in dest_result:
|
||||
# dest_contents = dest_result['content']
|
||||
# if dest_result['encoding'] == 'base64':
|
||||
# dest_contents = base64.b64decode(dest_contents)
|
||||
# else:
|
||||
# raise Exception("unknown encoding, failed: %s" % dest_result)
|
||||
xfered = self._transfer_data('src', resultant)
|
||||
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
self._remote_chmod('a+r', xfered, tmp)
|
||||
|
||||
# run the copy module
|
||||
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=xfered,
|
||||
dest=dest,
|
||||
original_basename=os.path.basename(src),
|
||||
)
|
||||
)
|
||||
|
||||
# FIXME: checkmode stuff
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=src, after=resultant))
|
||||
#else:
|
||||
# res = self.runner._execute_module(conn, tmp, 'copy', module_args_tmp, inject=inject)
|
||||
# res.diff = dict(after=resultant)
|
||||
# return res
|
||||
res = self._execute_module(module_name='copy', module_args=new_module_args, tmp=tmp)
|
||||
#res.diff = dict(after=resultant)
|
||||
return res
|
||||
else:
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=xfered,
|
||||
dest=dest,
|
||||
original_basename=os.path.basename(src),
|
||||
)
|
||||
)
|
||||
|
||||
return self._execute_module(module_name='file', module_args=new_module_args, tmp=tmp)
|
||||
65
lib/ansible/plugins/action/assert.py
Normal file
65
lib/ansible/plugins/action/assert.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copyright 2012, Dag Wieers <dag@wieers.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.playbook.conditional import Conditional
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
''' Fail with custom message '''
|
||||
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
if not 'that' in self._task.args:
|
||||
raise AnsibleError('conditional required in "that" string')
|
||||
|
||||
msg = None
|
||||
if 'msg' in self._task.args:
|
||||
msg = self._task.args['msg']
|
||||
|
||||
# make sure the 'that' items are a list
|
||||
thats = self._task.args['that']
|
||||
if not isinstance(thats, list):
|
||||
thats = [ thats ]
|
||||
|
||||
# Now we iterate over the that items, temporarily assigning them
|
||||
# to the task's when value so we can evaluate the conditional using
|
||||
# the built in evaluate function. The when has already been evaluated
|
||||
# by this point, and is not used again, so we don't care about mangling
|
||||
# that value now
|
||||
cond = Conditional(loader=self._loader)
|
||||
for that in thats:
|
||||
cond.when = [ that ]
|
||||
test_result = cond.evaluate_conditional(all_vars=task_vars)
|
||||
if not test_result:
|
||||
result = dict(
|
||||
failed = True,
|
||||
evaluated_to = test_result,
|
||||
assertion = that,
|
||||
)
|
||||
|
||||
if msg:
|
||||
result['msg'] = msg
|
||||
|
||||
return result
|
||||
|
||||
return dict(changed=False, msg='all assertions passed')
|
||||
|
||||
70
lib/ansible/plugins/action/async.py
Normal file
70
lib/ansible/plugins/action/async.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import random
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' transfer the given module name, plus the async module, then run it '''
|
||||
|
||||
# FIXME: noop stuff needs to be sorted ut
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
||||
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
module_name = self._task.action
|
||||
async_module_path = self._shell.join_path(tmp, 'async_wrapper')
|
||||
remote_module_path = self._shell.join_path(tmp, module_name)
|
||||
|
||||
env_string = self._compute_environment_string()
|
||||
|
||||
# configure, upload, and chmod the target module
|
||||
(module_style, shebang, module_data) = self._configure_module(module_name=module_name, module_args=self._task.args)
|
||||
self._transfer_data(remote_module_path, module_data)
|
||||
self._remote_chmod(tmp, 'a+rx', remote_module_path)
|
||||
|
||||
# configure, upload, and chmod the async_wrapper module
|
||||
(async_module_style, shebang, async_module_data) = self._configure_module(module_name='async_wrapper', module_args=dict())
|
||||
self._transfer_data(async_module_path, async_module_data)
|
||||
self._remote_chmod(tmp, 'a+rx', async_module_path)
|
||||
|
||||
argsfile = self._transfer_data(self._shell.join_path(tmp, 'arguments'), json.dumps(self._task.args))
|
||||
|
||||
async_limit = self._task.async
|
||||
async_jid = str(random.randint(0, 999999999999))
|
||||
|
||||
async_cmd = " ".join([str(x) for x in [async_module_path, async_jid, async_limit, remote_module_path, argsfile]])
|
||||
result = self._low_level_execute_command(cmd=async_cmd, tmp=None)
|
||||
|
||||
# clean up after
|
||||
if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES:
|
||||
self._remove_tmp_path(tmp)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
349
lib/ansible/plugins/action/copy.py
Normal file
349
lib/ansible/plugins/action/copy.py
Normal file
@@ -0,0 +1,349 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import pipes
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.boolean import boolean
|
||||
from ansible.utils.hashing import checksum
|
||||
from ansible.utils.unicode import to_bytes
|
||||
from ansible.parsing.vault import VaultLib
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' handler for file transfer operations '''
|
||||
|
||||
source = self._task.args.get('src', None)
|
||||
content = self._task.args.get('content', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
raw = boolean(self._task.args.get('raw', 'no'))
|
||||
force = boolean(self._task.args.get('force', 'yes'))
|
||||
|
||||
# FIXME: first available file needs to be reworked somehow...
|
||||
#if (source is None and content is None and not 'first_available_file' in inject) or dest is None:
|
||||
# result=dict(failed=True, msg="src (or content) and dest are required")
|
||||
# return ReturnData(conn=conn, result=result)
|
||||
#elif (source is not None or 'first_available_file' in inject) and content is not None:
|
||||
# result=dict(failed=True, msg="src and content are mutually exclusive")
|
||||
# return ReturnData(conn=conn, result=result)
|
||||
|
||||
# Check if the source ends with a "/"
|
||||
source_trailing_slash = False
|
||||
if source:
|
||||
source_trailing_slash = source.endswith(os.sep)
|
||||
|
||||
# Define content_tempfile in case we set it after finding content populated.
|
||||
content_tempfile = None
|
||||
|
||||
# If content is defined make a temp file and write the content into it.
|
||||
if content is not None:
|
||||
try:
|
||||
# If content comes to us as a dict it should be decoded json.
|
||||
# We need to encode it back into a string to write it out.
|
||||
if isinstance(content, dict):
|
||||
content_tempfile = self._create_content_tempfile(json.dumps(content))
|
||||
else:
|
||||
content_tempfile = self._create_content_tempfile(content)
|
||||
source = content_tempfile
|
||||
except Exception as err:
|
||||
return dict(failed=True, msg="could not write content temp file: %s" % err)
|
||||
|
||||
###############################################################################################
|
||||
# FIXME: first_available_file needs to be reworked?
|
||||
###############################################################################################
|
||||
# if we have first_available_file in our vars
|
||||
# look up the files and use the first one we find as src
|
||||
#elif 'first_available_file' in inject:
|
||||
# found = False
|
||||
# for fn in inject.get('first_available_file'):
|
||||
# fn_orig = fn
|
||||
# fnt = template.template(self.runner.basedir, fn, inject)
|
||||
# fnd = utils.path_dwim(self.runner.basedir, fnt)
|
||||
# if not os.path.exists(fnd) and '_original_file' in inject:
|
||||
# fnd = utils.path_dwim_relative(inject['_original_file'], 'files', fnt, self.runner.basedir, check=False)
|
||||
# if os.path.exists(fnd):
|
||||
# source = fnd
|
||||
# found = True
|
||||
# break
|
||||
# if not found:
|
||||
# results = dict(failed=True, msg="could not find src in first_available_file list")
|
||||
# return ReturnData(conn=conn, result=results)
|
||||
###############################################################################################
|
||||
else:
|
||||
if self._task._role is not None:
|
||||
source = self._loader.path_dwim_relative(self._task._role._role_path, 'files', source)
|
||||
else:
|
||||
source = self._loader.path_dwim(source)
|
||||
|
||||
# A list of source file tuples (full_path, relative_path) which will try to copy to the destination
|
||||
source_files = []
|
||||
|
||||
# If source is a directory populate our list else source is a file and translate it to a tuple.
|
||||
if os.path.isdir(source):
|
||||
# Get the amount of spaces to remove to get the relative path.
|
||||
if source_trailing_slash:
|
||||
sz = len(source)
|
||||
else:
|
||||
sz = len(source.rsplit('/', 1)[0]) + 1
|
||||
|
||||
# Walk the directory and append the file tuples to source_files.
|
||||
for base_path, sub_folders, files in os.walk(source):
|
||||
for file in files:
|
||||
full_path = os.path.join(base_path, file)
|
||||
rel_path = full_path[sz:]
|
||||
source_files.append((full_path, rel_path))
|
||||
|
||||
# If it's recursive copy, destination is always a dir,
|
||||
# explicitly mark it so (note - copy module relies on this).
|
||||
if not self._shell.path_has_trailing_slash(dest):
|
||||
dest = self._shell.join_path(dest, '')
|
||||
else:
|
||||
source_files.append((source, os.path.basename(source)))
|
||||
|
||||
changed = False
|
||||
diffs = []
|
||||
module_result = {"changed": False}
|
||||
|
||||
# A register for if we executed a module.
|
||||
# Used to cut down on command calls when not recursive.
|
||||
module_executed = False
|
||||
|
||||
# Tell _execute_module to delete the file if there is one file.
|
||||
delete_remote_tmp = (len(source_files) == 1)
|
||||
|
||||
# If this is a recursive action create a tmp path that we can share as the _exec_module create is too late.
|
||||
if not delete_remote_tmp:
|
||||
if tmp is None or "-tmp-" not in tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
# expand any user home dir specifier
|
||||
dest = self._remote_expand_user(dest, tmp)
|
||||
|
||||
for source_full, source_rel in source_files:
|
||||
|
||||
# Generate a hash of the local file.
|
||||
local_checksum = checksum(source_full)
|
||||
|
||||
# If local_checksum is not defined we can't find the file so we should fail out.
|
||||
if local_checksum is None:
|
||||
return dict(failed=True, msg="could not find src=%s" % source_full)
|
||||
|
||||
# This is kind of optimization - if user told us destination is
|
||||
# dir, do path manipulation right away, otherwise we still check
|
||||
# for dest being a dir via remote call below.
|
||||
if self._shell.path_has_trailing_slash(dest):
|
||||
dest_file = self._shell.join_path(dest, source_rel)
|
||||
else:
|
||||
dest_file = self._shell.join_path(dest)
|
||||
|
||||
# Attempt to get the remote checksum
|
||||
remote_checksum = self._remote_checksum(tmp, dest_file)
|
||||
|
||||
if remote_checksum == '3':
|
||||
# The remote_checksum was executed on a directory.
|
||||
if content is not None:
|
||||
# If source was defined as content remove the temporary file and fail out.
|
||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||
return dict(failed=True, msg="can not use content with a dir as dest")
|
||||
else:
|
||||
# Append the relative source location to the destination and retry remote_checksum
|
||||
dest_file = self._shell.join_path(dest, source_rel)
|
||||
remote_checksum = self._remote_checksum(tmp, dest_file)
|
||||
|
||||
if remote_checksum != '1' and not force:
|
||||
# remote_file does not exist so continue to next iteration.
|
||||
continue
|
||||
|
||||
if local_checksum != remote_checksum:
|
||||
# The checksums don't match and we will change or error out.
|
||||
changed = True
|
||||
|
||||
# Create a tmp path if missing only if this is not recursive.
|
||||
# If this is recursive we already have a tmp path.
|
||||
if delete_remote_tmp:
|
||||
if tmp is None or "-tmp-" not in tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
# FIXME: runner shouldn't have the diff option there
|
||||
#if self.runner.diff and not raw:
|
||||
# diff = self._get_diff_data(tmp, dest_file, source_full)
|
||||
#else:
|
||||
# diff = {}
|
||||
diff = {}
|
||||
|
||||
# FIXME: noop stuff
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||
# diffs.append(diff)
|
||||
# changed = True
|
||||
# module_result = dict(changed=True)
|
||||
# continue
|
||||
|
||||
# Define a remote directory that we will copy the file to.
|
||||
tmp_src = tmp + 'source'
|
||||
|
||||
if not raw:
|
||||
self._connection.put_file(source_full, tmp_src)
|
||||
else:
|
||||
self._connection.put_file(source_full, dest_file)
|
||||
|
||||
# We have copied the file remotely and no longer require our content_tempfile
|
||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
self._remote_chmod('a+r', tmp_src, tmp)
|
||||
|
||||
if raw:
|
||||
# Continue to next iteration if raw is defined.
|
||||
continue
|
||||
|
||||
# Run the copy module
|
||||
|
||||
# src and dest here come after original and override them
|
||||
# we pass dest only to make sure it includes trailing slash in case of recursive copy
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=tmp_src,
|
||||
dest=dest,
|
||||
original_basename=source_rel,
|
||||
)
|
||||
)
|
||||
|
||||
module_return = self._execute_module(module_name='copy', module_args=new_module_args, delete_remote_tmp=delete_remote_tmp)
|
||||
module_executed = True
|
||||
|
||||
else:
|
||||
# no need to transfer the file, already correct hash, but still need to call
|
||||
# the file module in case we want to change attributes
|
||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||
|
||||
if raw:
|
||||
# Continue to next iteration if raw is defined.
|
||||
# self._remove_tmp_path(tmp)
|
||||
continue
|
||||
|
||||
# Build temporary module_args.
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=source_rel,
|
||||
dest=dest,
|
||||
original_basename=source_rel
|
||||
)
|
||||
)
|
||||
|
||||
# Execute the file module.
|
||||
module_return = self._execute_module(module_name='file', module_args=new_module_args, delete_remote_tmp=delete_remote_tmp)
|
||||
module_executed = True
|
||||
|
||||
if not module_return.get('checksum'):
|
||||
module_return['checksum'] = local_checksum
|
||||
if module_return.get('failed') == True:
|
||||
return module_return
|
||||
if module_return.get('changed') == True:
|
||||
changed = True
|
||||
|
||||
# the file module returns the file path as 'path', but
|
||||
# the copy module uses 'dest', so add it if it's not there
|
||||
if 'path' in module_return and 'dest' not in module_return:
|
||||
module_return['dest'] = module_return['path']
|
||||
|
||||
# Delete tmp path if we were recursive or if we did not execute a module.
|
||||
if (not C.DEFAULT_KEEP_REMOTE_FILES and not delete_remote_tmp) or (not C.DEFAULT_KEEP_REMOTE_FILES and delete_remote_tmp and not module_executed):
|
||||
self._remove_tmp_path(tmp)
|
||||
|
||||
# TODO: Support detailed status/diff for multiple files
|
||||
if module_executed and len(source_files) == 1:
|
||||
result = module_return
|
||||
else:
|
||||
result = dict(dest=dest, src=source, changed=changed)
|
||||
|
||||
if len(diffs) == 1:
|
||||
result['diff']=diffs[0]
|
||||
|
||||
return result
|
||||
|
||||
def _create_content_tempfile(self, content):
|
||||
''' Create a tempfile containing defined content '''
|
||||
fd, content_tempfile = tempfile.mkstemp()
|
||||
f = os.fdopen(fd, 'wb')
|
||||
content = to_bytes(content)
|
||||
try:
|
||||
f.write(content)
|
||||
except Exception as err:
|
||||
os.remove(content_tempfile)
|
||||
raise Exception(err)
|
||||
finally:
|
||||
f.close()
|
||||
return content_tempfile
|
||||
|
||||
def _get_diff_data(self, tmp, destination, source):
|
||||
peek_result = self._execute_module(module_name='file', module_args=dict(path=destination, diff_peek=True), persist_files=True)
|
||||
if 'failed' in peek_result and peek_result['failed'] or peek_result.get('rc', 0) != 0:
|
||||
return {}
|
||||
|
||||
diff = {}
|
||||
if peek_result['state'] == 'absent':
|
||||
diff['before'] = ''
|
||||
elif peek_result['appears_binary']:
|
||||
diff['dst_binary'] = 1
|
||||
# FIXME: this should not be in utils..
|
||||
#elif peek_result['size'] > utils.MAX_FILE_SIZE_FOR_DIFF:
|
||||
# diff['dst_larger'] = utils.MAX_FILE_SIZE_FOR_DIFF
|
||||
else:
|
||||
dest_result = self._execute_module(module_name='slurp', module_args=dict(path=destination), tmp=tmp, persist_files=True)
|
||||
if 'content' in dest_result:
|
||||
dest_contents = dest_result['content']
|
||||
if dest_result['encoding'] == 'base64':
|
||||
dest_contents = base64.b64decode(dest_contents)
|
||||
else:
|
||||
raise Exception("unknown encoding, failed: %s" % dest_result)
|
||||
diff['before_header'] = destination
|
||||
diff['before'] = dest_contents
|
||||
|
||||
src = open(source)
|
||||
src_contents = src.read(8192)
|
||||
st = os.stat(source)
|
||||
if "\x00" in src_contents:
|
||||
diff['src_binary'] = 1
|
||||
# FIXME: this should not be in utils
|
||||
#elif st[stat.ST_SIZE] > utils.MAX_FILE_SIZE_FOR_DIFF:
|
||||
# diff['src_larger'] = utils.MAX_FILE_SIZE_FOR_DIFF
|
||||
else:
|
||||
src.seek(0)
|
||||
diff['after_header'] = source
|
||||
diff['after'] = src.read()
|
||||
|
||||
return diff
|
||||
|
||||
def _remove_tempfile_if_content_defined(self, content, content_tempfile):
|
||||
if content is not None:
|
||||
os.remove(content_tempfile)
|
||||
|
||||
48
lib/ansible/plugins/action/debug.py
Normal file
48
lib/ansible/plugins/action/debug.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Copyright 2012, Dag Wieers <dag@wieers.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.boolean import boolean
|
||||
from ansible.template import Templar
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
''' Print statements during execution '''
|
||||
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
if 'msg' in self._task.args:
|
||||
if 'fail' in self._task.args and boolean(self._task.args['fail']):
|
||||
result = dict(failed=True, msg=self._task.args['msg'])
|
||||
else:
|
||||
result = dict(msg=self._task.args['msg'])
|
||||
# FIXME: move the LOOKUP_REGEX somewhere else
|
||||
elif 'var' in self._task.args: # and not utils.LOOKUP_REGEX.search(self._task.args['var']):
|
||||
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=task_vars)
|
||||
results = templar.template(self._task.args['var'], convert_bare=True)
|
||||
result = dict()
|
||||
result[self._task.args['var']] = results
|
||||
else:
|
||||
result = dict(msg='here we are')
|
||||
|
||||
# force flag to make debug output module always verbose
|
||||
result['verbose_always'] = True
|
||||
|
||||
return result
|
||||
35
lib/ansible/plugins/action/fail.py
Normal file
35
lib/ansible/plugins/action/fail.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2012, Dag Wieers <dag@wieers.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
''' Fail with custom message '''
|
||||
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
msg = 'Failed as requested from task'
|
||||
if self._task.args and 'msg' in self._task.args:
|
||||
msg = self._task.args.get('msg')
|
||||
|
||||
return dict(failed=True, msg=msg)
|
||||
|
||||
158
lib/ansible/plugins/action/fetch.py
Normal file
158
lib/ansible/plugins/action/fetch.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import random
|
||||
import traceback
|
||||
import tempfile
|
||||
import base64
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.boolean import boolean
|
||||
from ansible.utils.hashing import checksum, checksum_s, md5, secure_hash
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' handler for fetch operations '''
|
||||
|
||||
# FIXME: is this even required anymore?
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not (yet) supported for this module'))
|
||||
|
||||
source = self._task.args.get('src', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
flat = boolean(self._task.args.get('flat'))
|
||||
fail_on_missing = boolean(self._task.args.get('fail_on_missing'))
|
||||
validate_checksum = boolean(self._task.args.get('validate_checksum', self._task.args.get('validate_md5')))
|
||||
|
||||
if 'validate_md5' in self._task.args and 'validate_checksum' in self._task.args:
|
||||
return dict(failed=True, msg="validate_checksum and validate_md5 cannot both be specified")
|
||||
|
||||
if source is None or dest is None:
|
||||
return dict(failed=True, msg="src and dest are required")
|
||||
|
||||
source = self._shell.join_path(source)
|
||||
source = self._remote_expand_user(source, tmp)
|
||||
|
||||
# calculate checksum for the remote file
|
||||
remote_checksum = self._remote_checksum(tmp, source)
|
||||
|
||||
# use slurp if sudo and permissions are lacking
|
||||
remote_data = None
|
||||
if remote_checksum in ('1', '2') or self._connection_info.become:
|
||||
slurpres = self._execute_module(module_name='slurp', module_args=dict(src=source), tmp=tmp)
|
||||
if slurpres.get('rc') == 0:
|
||||
if slurpres['encoding'] == 'base64':
|
||||
remote_data = base64.b64decode(slurpres['content'])
|
||||
if remote_data is not None:
|
||||
remote_checksum = checksum_s(remote_data)
|
||||
# the source path may have been expanded on the
|
||||
# target system, so we compare it here and use the
|
||||
# expanded version if it's different
|
||||
remote_source = slurpres.get('source')
|
||||
if remote_source and remote_source != source:
|
||||
source = remote_source
|
||||
else:
|
||||
# FIXME: should raise an error here? the old code did nothing
|
||||
pass
|
||||
|
||||
# calculate the destination name
|
||||
if os.path.sep not in self._shell.join_path('a', ''):
|
||||
source_local = source.replace('\\', '/')
|
||||
else:
|
||||
source_local = source
|
||||
|
||||
dest = os.path.expanduser(dest)
|
||||
if flat:
|
||||
if dest.endswith(os.sep):
|
||||
# if the path ends with "/", we'll use the source filename as the
|
||||
# destination filename
|
||||
base = os.path.basename(source_local)
|
||||
dest = os.path.join(dest, base)
|
||||
if not dest.startswith("/"):
|
||||
# if dest does not start with "/", we'll assume a relative path
|
||||
dest = self._loader.path_dwim(dest)
|
||||
else:
|
||||
# files are saved in dest dir, with a subdir for each host, then the filename
|
||||
if 'inventory_hostname' in task_vars:
|
||||
target_name = task_vars['inventory_hostname']
|
||||
else:
|
||||
target_name = self._connection_info.remote_addr
|
||||
dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local)
|
||||
|
||||
dest = dest.replace("//","/")
|
||||
|
||||
if remote_checksum in ('0', '1', '2', '3', '4'):
|
||||
# these don't fail because you may want to transfer a log file that possibly MAY exist
|
||||
# but keep going to fetch other log files
|
||||
if remote_checksum == '0':
|
||||
result = dict(msg="unable to calculate the checksum of the remote file", file=source, changed=False)
|
||||
elif remote_checksum == '1':
|
||||
if fail_on_missing:
|
||||
result = dict(failed=True, msg="the remote file does not exist", file=source)
|
||||
else:
|
||||
result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False)
|
||||
elif remote_checksum == '2':
|
||||
result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False)
|
||||
elif remote_checksum == '3':
|
||||
result = dict(msg="remote file is a directory, fetch cannot work on directories", file=source, changed=False)
|
||||
elif remote_checksum == '4':
|
||||
result = dict(msg="python isn't present on the system. Unable to compute checksum", file=source, changed=False)
|
||||
return result
|
||||
|
||||
# calculate checksum for the local file
|
||||
local_checksum = checksum(dest)
|
||||
|
||||
if remote_checksum != local_checksum:
|
||||
# create the containing directories, if needed
|
||||
if not os.path.isdir(os.path.dirname(dest)):
|
||||
os.makedirs(os.path.dirname(dest))
|
||||
|
||||
# fetch the file and check for changes
|
||||
if remote_data is None:
|
||||
self._connection.fetch_file(source, dest)
|
||||
else:
|
||||
f = open(dest, 'w')
|
||||
f.write(remote_data)
|
||||
f.close()
|
||||
new_checksum = secure_hash(dest)
|
||||
# For backwards compatibility. We'll return None on FIPS enabled
|
||||
# systems
|
||||
try:
|
||||
new_md5 = md5(dest)
|
||||
except ValueError:
|
||||
new_md5 = None
|
||||
|
||||
if validate_checksum and new_checksum != remote_checksum:
|
||||
return dict(failed=True, md5sum=new_md5, msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)
|
||||
return dict(changed=True, md5sum=new_md5, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)
|
||||
else:
|
||||
# For backwards compatibility. We'll return None on FIPS enabled
|
||||
# systems
|
||||
try:
|
||||
local_md5 = md5(dest)
|
||||
except ValueError:
|
||||
local_md5 = None
|
||||
|
||||
return dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)
|
||||
|
||||
39
lib/ansible/plugins/action/group_by.py
Normal file
39
lib/ansible/plugins/action/group_by.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright 2012, Jeroen Hoekx <jeroen@hoekx.be>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
''' Create inventory groups based on variables '''
|
||||
|
||||
### We need to be able to modify the inventory
|
||||
BYPASS_HOST_LOOP = True
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
if not 'key' in self._task.args:
|
||||
return dict(failed=True, msg="the 'key' param is required when using group_by")
|
||||
|
||||
group_name = self._task.args.get('key')
|
||||
group_name = group_name.replace(' ','-')
|
||||
|
||||
return dict(changed=True, add_group=group_name)
|
||||
|
||||
50
lib/ansible/plugins/action/include_vars.py
Normal file
50
lib/ansible/plugins/action/include_vars.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# (c) 2013-2014, Benno Joy <benno@ansible.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from types import NoneType
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.parsing import DataLoader
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
source = self._task.args.get('_raw_params')
|
||||
|
||||
if self._task._role:
|
||||
source = self._loader.path_dwim_relative(self._task._role._role_path, 'vars', source)
|
||||
else:
|
||||
source = self._loader.path_dwim(source)
|
||||
|
||||
if os.path.exists(source):
|
||||
data = self._loader.load_from_file(source)
|
||||
if data is None:
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
raise AnsibleError("%s must be stored as a dictionary/hash" % source)
|
||||
return dict(ansible_facts=data)
|
||||
else:
|
||||
return dict(failed=True, msg="Source file not found.", file=source)
|
||||
|
||||
29
lib/ansible/plugins/action/normal.py
Normal file
29
lib/ansible/plugins/action/normal.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
#vv("REMOTE_MODULE %s %s" % (module_name, module_args), host=conn.host)
|
||||
return self._execute_module(tmp)
|
||||
|
||||
|
||||
66
lib/ansible/plugins/action/patch.py
Normal file
66
lib/ansible/plugins/action/patch.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# (c) 2015, Brian Coca <briancoca+dev@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
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.boolean import boolean
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
src = self._task.args.get('src', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
remote_src = boolean(self._task.args.get('remote_src', 'no'))
|
||||
|
||||
if src is None:
|
||||
return dict(failed=True, msg="src is required")
|
||||
elif remote_src:
|
||||
# everything is remote, so we just execute the module
|
||||
# without changing any of the module arguments
|
||||
return self._execute_module()
|
||||
|
||||
if self._task._role is not None:
|
||||
src = self._loader.path_dwim_relative(self._task._role._role_path, 'files', src)
|
||||
else:
|
||||
src = self._loader.path_dwim(src)
|
||||
|
||||
# create the remote tmp dir if needed, and put the source file there
|
||||
if tmp is None or "-tmp-" not in tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
tmp_src = self._shell.join_path(tmp, os.path.basename(src))
|
||||
self._connection.put_file(src, tmp_src)
|
||||
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
# FIXME: noop stuff here
|
||||
#if not self.runner.noop_on_check(inject):
|
||||
# self._remote_chmod('a+r', tmp_src, tmp)
|
||||
self._remote_chmod('a+r', tmp_src, tmp)
|
||||
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=tmp_src,
|
||||
)
|
||||
)
|
||||
|
||||
return self._execute_module('patch', module_args=new_module_args)
|
||||
136
lib/ansible/plugins/action/pause.py
Normal file
136
lib/ansible/plugins/action/pause.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# Copyright 2012, Tim Bielawa <tbielawa@redhat.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
import time
|
||||
|
||||
from termios import tcflush, TCIFLUSH
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
''' pauses execution for a length or time, or until input is received '''
|
||||
|
||||
PAUSE_TYPES = ['seconds', 'minutes', 'prompt', '']
|
||||
BYPASS_HOST_LOOP = True
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' run the pause action module '''
|
||||
|
||||
duration_unit = 'minutes'
|
||||
prompt = None
|
||||
seconds = None
|
||||
result = dict(
|
||||
changed = False,
|
||||
rc = 0,
|
||||
stderr = '',
|
||||
stdout = '',
|
||||
start = None,
|
||||
stop = None,
|
||||
delta = None,
|
||||
)
|
||||
|
||||
# FIXME: not sure if we can get this info directly like this anymore?
|
||||
#hosts = ', '.join(self.runner.host_set)
|
||||
|
||||
# Is 'args' empty, then this is the default prompted pause
|
||||
if self._task.args is None or len(self._task.args.keys()) == 0:
|
||||
pause_type = 'prompt'
|
||||
#prompt = "[%s]\nPress enter to continue:\n" % hosts
|
||||
prompt = "[%s]\nPress enter to continue:\n" % self._task.get_name().strip()
|
||||
|
||||
# Are 'minutes' or 'seconds' keys that exist in 'args'?
|
||||
elif 'minutes' in self._task.args or 'seconds' in self._task.args:
|
||||
try:
|
||||
if 'minutes' in self._task.args:
|
||||
pause_type = 'minutes'
|
||||
# The time() command operates in seconds so we need to
|
||||
# recalculate for minutes=X values.
|
||||
seconds = int(self._task.args['minutes']) * 60
|
||||
else:
|
||||
pause_type = 'seconds'
|
||||
seconds = int(self._task.args['seconds'])
|
||||
duration_unit = 'seconds'
|
||||
|
||||
except ValueError as e:
|
||||
return dict(failed=True, msg="non-integer value given for prompt duration:\n%s" % str(e))
|
||||
|
||||
# Is 'prompt' a key in 'args'?
|
||||
elif 'prompt' in self._task.args:
|
||||
pause_type = 'prompt'
|
||||
#prompt = "[%s]\n%s:\n" % (hosts, self._task.args['prompt'])
|
||||
prompt = "[%s]\n%s:\n" % (self._task.get_name().strip(), self._task.args['prompt'])
|
||||
|
||||
# I have no idea what you're trying to do. But it's so wrong.
|
||||
else:
|
||||
return dict(failed=True, msg="invalid pause type given. must be one of: %s" % ", ".join(self.PAUSE_TYPES))
|
||||
|
||||
#vv("created 'pause' ActionModule: pause_type=%s, duration_unit=%s, calculated_seconds=%s, prompt=%s" % \
|
||||
# (self.pause_type, self.duration_unit, self.seconds, self.prompt))
|
||||
|
||||
########################################################################
|
||||
# Begin the hard work!
|
||||
|
||||
start = time.time()
|
||||
result['start'] = str(datetime.datetime.now())
|
||||
|
||||
|
||||
# FIXME: this is all very broken right now, as prompting from the worker side
|
||||
# is not really going to be supported, and actions marked as BYPASS_HOST_LOOP
|
||||
# probably should not be run through the executor engine at all. Also, ctrl+c
|
||||
# is now captured on the parent thread, so it can't be caught here via the
|
||||
# KeyboardInterrupt exception.
|
||||
|
||||
try:
|
||||
if not pause_type == 'prompt':
|
||||
print("(^C-c = continue early, ^C-a = abort)")
|
||||
#print("[%s]\nPausing for %s seconds" % (hosts, seconds))
|
||||
print("[%s]\nPausing for %s seconds" % (self._task.get_name().strip(), seconds))
|
||||
time.sleep(seconds)
|
||||
else:
|
||||
# Clear out any unflushed buffered input which would
|
||||
# otherwise be consumed by raw_input() prematurely.
|
||||
#tcflush(sys.stdin, TCIFLUSH)
|
||||
result['user_input'] = raw_input(prompt.encode(sys.stdout.encoding))
|
||||
except KeyboardInterrupt:
|
||||
while True:
|
||||
print('\nAction? (a)bort/(c)ontinue: ')
|
||||
c = getch()
|
||||
if c == 'c':
|
||||
# continue playbook evaluation
|
||||
break
|
||||
elif c == 'a':
|
||||
# abort further playbook evaluation
|
||||
raise ae('user requested abort!')
|
||||
finally:
|
||||
duration = time.time() - start
|
||||
result['stop'] = str(datetime.datetime.now())
|
||||
result['delta'] = int(duration)
|
||||
|
||||
if duration_unit == 'minutes':
|
||||
duration = round(duration / 60.0, 2)
|
||||
else:
|
||||
duration = round(duration, 2)
|
||||
|
||||
result['stdout'] = "Paused for %s %s" % (duration, duration_unit)
|
||||
|
||||
return result
|
||||
|
||||
41
lib/ansible/plugins/action/raw.py
Normal file
41
lib/ansible/plugins/action/raw.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
|
||||
# FIXME: need to rework the noop stuff still
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# # in --check mode, always skip this module execution
|
||||
# return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True))
|
||||
|
||||
executable = self._task.args.get('executable')
|
||||
result = self._low_level_execute_command(self._task.args.get('_raw_params'), tmp=tmp, executable=executable)
|
||||
|
||||
# for some modules (script, raw), the sudo success key
|
||||
# may leak into the stdout due to the way the sudo/su
|
||||
# command is constructed, so we filter that out here
|
||||
if result.get('stdout','').strip().startswith('SUDO-SUCCESS-'):
|
||||
result['stdout'] = re.sub(r'^((\r)?\n)?SUDO-SUCCESS.*(\r)?\n', '', result['stdout'])
|
||||
|
||||
return result
|
||||
98
lib/ansible/plugins/action/script.py
Normal file
98
lib/ansible/plugins/action/script.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
TRANSFERS_FILES = True
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
''' handler for file transfer operations '''
|
||||
|
||||
# FIXME: noop stuff still needs to be sorted out
|
||||
#if self.runner.noop_on_check(inject):
|
||||
# # in check mode, always skip this module
|
||||
# return ReturnData(conn=conn, comm_ok=True,
|
||||
# result=dict(skipped=True, msg='check mode not supported for this module'))
|
||||
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
creates = self._task.args.get('creates')
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
result = self._execute_module(module_name='stat', module_args=dict(path=creates), tmp=tmp, persist_files=True)
|
||||
stat = result.get('stat', None)
|
||||
if stat and stat.get('exists', False):
|
||||
return dict(skipped=True, msg=("skipped, since %s exists" % creates))
|
||||
|
||||
removes = self._task.args.get('removes')
|
||||
if removes:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename does not exist. This allows idempotence
|
||||
# of command executions.
|
||||
result = self._execute_module(module_name='stat', module_args=dict(path=removes), tmp=tmp, persist_files=True)
|
||||
stat = result.get('stat', None)
|
||||
if stat and not stat.get('exists', False):
|
||||
return dict(skipped=True, msg=("skipped, since %s does not exist" % removes))
|
||||
|
||||
# the script name is the first item in the raw params, so we split it
|
||||
# out now so we know the file name we need to transfer to the remote,
|
||||
# and everything else is an argument to the script which we need later
|
||||
# to append to the remote command
|
||||
parts = self._task.args.get('_raw_params', '').strip().split()
|
||||
source = parts[0]
|
||||
args = ' '.join(parts[1:])
|
||||
|
||||
if self._task._role is not None:
|
||||
source = self._loader.path_dwim_relative(self._task._role._role_path, 'files', source)
|
||||
else:
|
||||
source = self._loader.path_dwim(source)
|
||||
|
||||
# transfer the file to a remote tmp location
|
||||
tmp_src = self._shell.join_path(tmp, os.path.basename(source))
|
||||
self._connection.put_file(source, tmp_src)
|
||||
|
||||
sudoable = True
|
||||
# set file permissions, more permissive when the copy is done as a different user
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
chmod_mode = 'a+rx'
|
||||
sudoable = False
|
||||
else:
|
||||
chmod_mode = '+rx'
|
||||
self._remote_chmod(tmp, chmod_mode, tmp_src, sudoable=sudoable)
|
||||
|
||||
# add preparation steps to one ssh roundtrip executing the script
|
||||
env_string = self._compute_environment_string()
|
||||
script_cmd = ' '.join([env_string, tmp_src, args])
|
||||
|
||||
result = self._low_level_execute_command(cmd=script_cmd, tmp=None, sudoable=sudoable)
|
||||
|
||||
# clean up after
|
||||
if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES:
|
||||
self._remove_tmp_path(tmp)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
return result
|
||||
38
lib/ansible/plugins/action/set_fact.py
Normal file
38
lib/ansible/plugins/action/set_fact.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright 2013 Dag Wieers <dag@wieers.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.template import Templar
|
||||
from ansible.utils.boolean import boolean
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
TRANSFERS_FILES = False
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
templar = Templar(loader=self._loader, variables=task_vars)
|
||||
facts = dict()
|
||||
if self._task.args:
|
||||
for (k, v) in self._task.args.iteritems():
|
||||
k = templar.template(k)
|
||||
if isinstance(v, basestring) and v.lower() in ('true', 'false', 'yes', 'no'):
|
||||
v = boolean(v)
|
||||
facts[k] = v
|
||||
return dict(changed=False, ansible_facts=facts)
|
||||
169
lib/ansible/plugins/action/synchronize.py
Normal file
169
lib/ansible/plugins/action/synchronize.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012-2013, Timothy Appnel <tim@appnel.com>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os.path
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.boolean import boolean
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def _get_absolute_path(self, path):
|
||||
if self._task._role is not None:
|
||||
original_path = path
|
||||
path = self._loader.path_dwim_relative(self._task._role._role_path, 'files', path)
|
||||
if original_path and original_path[-1] == '/' and path[-1] != '/':
|
||||
# make sure the dwim'd path ends in a trailing "/"
|
||||
# if the original path did
|
||||
path += '/'
|
||||
|
||||
return path
|
||||
|
||||
def _process_origin(self, host, path, user):
|
||||
|
||||
if not host in ['127.0.0.1', 'localhost']:
|
||||
if user:
|
||||
return '%s@%s:%s' % (user, host, path)
|
||||
else:
|
||||
return '%s:%s' % (host, path)
|
||||
else:
|
||||
if not ':' in path:
|
||||
if not path.startswith('/'):
|
||||
path = self._get_absolute_path(path=path)
|
||||
return path
|
||||
|
||||
def _process_remote(self, host, task, path, user):
|
||||
transport = self._connection_info.connection
|
||||
return_data = None
|
||||
if not host in ['127.0.0.1', 'localhost'] or transport != "local":
|
||||
if user:
|
||||
return_data = '%s@%s:%s' % (user, host, path)
|
||||
else:
|
||||
return_data = '%s:%s' % (host, path)
|
||||
else:
|
||||
return_data = path
|
||||
|
||||
if not ':' in return_data:
|
||||
if not return_data.startswith('/'):
|
||||
return_data = self._get_absolute_path(path=return_data)
|
||||
|
||||
return return_data
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' generates params and passes them on to the rsync module '''
|
||||
|
||||
original_transport = task_vars.get('ansible_connection', self._connection_info.connection)
|
||||
transport_overridden = False
|
||||
if task_vars.get('delegate_to') is None:
|
||||
task_vars['delegate_to'] = '127.0.0.1'
|
||||
# IF original transport is not local, override transport and disable sudo.
|
||||
if original_transport != 'local':
|
||||
task_vars['ansible_connection'] = 'local'
|
||||
transport_overridden = True
|
||||
self.runner.sudo = False
|
||||
|
||||
src = self._task.args.get('src', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
|
||||
# FIXME: this doesn't appear to be used anywhere?
|
||||
local_rsync_path = task_vars.get('ansible_rsync_path')
|
||||
|
||||
# from the perspective of the rsync call the delegate is the localhost
|
||||
src_host = '127.0.0.1'
|
||||
dest_host = task_vars.get('ansible_ssh_host', task_vars.get('inventory_hostname'))
|
||||
|
||||
# allow ansible_ssh_host to be templated
|
||||
dest_is_local = dest_host in ['127.0.0.1', 'localhost']
|
||||
|
||||
# CHECK FOR NON-DEFAULT SSH PORT
|
||||
dest_port = self._task.args.get('dest_port')
|
||||
inv_port = task_vars.get('ansible_ssh_port', task_vars.get('inventory_hostname'))
|
||||
if inv_port != dest_port and inv_port != task_vars.get('inventory_hostname'):
|
||||
dest_port = inv_port
|
||||
|
||||
# edge case: explicit delegate and dest_host are the same
|
||||
if dest_host == task_vars.get('delegate_to'):
|
||||
dest_host = '127.0.0.1'
|
||||
|
||||
# SWITCH SRC AND DEST PER MODE
|
||||
if self._task.args.get('mode', 'push') == 'pull':
|
||||
(dest_host, src_host) = (src_host, dest_host)
|
||||
|
||||
# CHECK DELEGATE HOST INFO
|
||||
use_delegate = False
|
||||
# FIXME: not sure if this is in connection info yet or not...
|
||||
#if conn.delegate != conn.host:
|
||||
# if 'hostvars' in task_vars:
|
||||
# if conn.delegate in task_vars['hostvars'] and original_transport != 'local':
|
||||
# # use a delegate host instead of localhost
|
||||
# use_delegate = True
|
||||
|
||||
# COMPARE DELEGATE, HOST AND TRANSPORT
|
||||
process_args = False
|
||||
if not dest_host is src_host and original_transport != 'local':
|
||||
# interpret and task_vars remote host info into src or dest
|
||||
process_args = True
|
||||
|
||||
# MUNGE SRC AND DEST PER REMOTE_HOST INFO
|
||||
if process_args or use_delegate:
|
||||
|
||||
user = None
|
||||
if boolean(task_vars.get('set_remote_user', 'yes')):
|
||||
if use_delegate:
|
||||
user = task_vars['hostvars'][conn.delegate].get('ansible_ssh_user')
|
||||
|
||||
if not use_delegate or not user:
|
||||
user = task_vars.get('ansible_ssh_user', self.runner.remote_user)
|
||||
|
||||
if use_delegate:
|
||||
# FIXME
|
||||
private_key = task_vars.get('ansible_ssh_private_key_file', self.runner.private_key_file)
|
||||
else:
|
||||
private_key = task_vars.get('ansible_ssh_private_key_file', self.runner.private_key_file)
|
||||
|
||||
if private_key is not None:
|
||||
private_key = os.path.expanduser(private_key)
|
||||
|
||||
# use the mode to define src and dest's url
|
||||
if self._task.args.get('mode', 'push') == 'pull':
|
||||
# src is a remote path: <user>@<host>, dest is a local path
|
||||
src = self._process_remote(src_host, src, user)
|
||||
dest = self._process_origin(dest_host, dest, user)
|
||||
else:
|
||||
# src is a local path, dest is a remote path: <user>@<host>
|
||||
src = self._process_origin(src_host, src, user)
|
||||
dest = self._process_remote(dest_host, dest, user)
|
||||
|
||||
# Allow custom rsync path argument.
|
||||
rsync_path = self._task.args.get('rsync_path', None)
|
||||
|
||||
# If no rsync_path is set, sudo was originally set, and dest is remote then add 'sudo rsync' argument.
|
||||
if not rsync_path and transport_overridden and self._connection_info.become and self._connection_info.become_method == 'sudo' and not dest_is_local:
|
||||
rsync_path = 'sudo rsync'
|
||||
|
||||
# make sure rsync path is quoted.
|
||||
if rsync_path:
|
||||
self._task.args['rsync_path'] = '"%s"' % rsync_path
|
||||
|
||||
# run the module and store the result
|
||||
result = self._execute_module('synchronize')
|
||||
|
||||
return result
|
||||
|
||||
186
lib/ansible/plugins/action/template.py
Normal file
186
lib/ansible/plugins/action/template.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# (c) 2015, 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import os
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.template import Templar
|
||||
from ansible.utils.hashing import checksum_s
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
TRANSFERS_FILES = True
|
||||
|
||||
def get_checksum(self, tmp, dest, try_directory=False, source=None):
|
||||
remote_checksum = self._remote_checksum(tmp, dest)
|
||||
|
||||
if remote_checksum in ('0', '2', '3', '4'):
|
||||
# Note: 1 means the file is not present which is fine; template
|
||||
# will create it. 3 means directory was specified instead of file
|
||||
if try_directory and remote_checksum == '3' and source:
|
||||
base = os.path.basename(source)
|
||||
dest = os.path.join(dest, base)
|
||||
remote_checksum = self.get_checksum(tmp, dest, try_directory=False)
|
||||
if remote_checksum not in ('0', '2', '3', '4'):
|
||||
return remote_checksum
|
||||
|
||||
result = dict(failed=True, msg="failed to checksum remote file."
|
||||
" Checksum error code: %s" % remote_checksum)
|
||||
return result
|
||||
|
||||
return remote_checksum
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' handler for template operations '''
|
||||
|
||||
source = self._task.args.get('src', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
|
||||
if (source is None and 'first_available_file' not in task_vars) or dest is None:
|
||||
return dict(failed=True, msg="src and dest are required")
|
||||
|
||||
if tmp is None:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
##################################################################################################
|
||||
# FIXME: this all needs to be sorted out
|
||||
##################################################################################################
|
||||
# if we have first_available_file in our vars
|
||||
# look up the files and use the first one we find as src
|
||||
#if 'first_available_file' in task_vars:
|
||||
# found = False
|
||||
# for fn in task_vars.get('first_available_file'):
|
||||
# fn_orig = fn
|
||||
# fnt = template.template(self.runner.basedir, fn, task_vars)
|
||||
# fnd = utils.path_dwim(self.runner.basedir, fnt)
|
||||
# if not os.path.exists(fnd) and '_original_file' in task_vars:
|
||||
# fnd = utils.path_dwim_relative(task_vars['_original_file'], 'templates', fnt, self.runner.basedir, check=False)
|
||||
# if os.path.exists(fnd):
|
||||
# source = fnd
|
||||
# found = True
|
||||
# break
|
||||
# if not found:
|
||||
# result = dict(failed=True, msg="could not find src in first_available_file list")
|
||||
# return ReturnData(conn=conn, comm_ok=False, result=result)
|
||||
#else:
|
||||
if 1:
|
||||
if self._task._role is not None:
|
||||
source = self._loader.path_dwim_relative(self._task._role._role_path, 'templates', source)
|
||||
else:
|
||||
source = self._loader.path_dwim(source)
|
||||
##################################################################################################
|
||||
# END FIXME
|
||||
##################################################################################################
|
||||
|
||||
# Expand any user home dir specification
|
||||
dest = self._remote_expand_user(dest, tmp)
|
||||
|
||||
directory_prepended = False
|
||||
if dest.endswith(os.sep):
|
||||
directory_prepended = True
|
||||
base = os.path.basename(source)
|
||||
dest = os.path.join(dest, base)
|
||||
|
||||
# template the source data locally & get ready to transfer
|
||||
templar = Templar(loader=self._loader, variables=task_vars)
|
||||
try:
|
||||
with open(source, 'r') as f:
|
||||
template_data = f.read()
|
||||
resultant = templar.template(template_data, preserve_trailing_newlines=True)
|
||||
except Exception as e:
|
||||
return dict(failed=True, msg=type(e).__name__ + ": " + str(e))
|
||||
|
||||
local_checksum = checksum_s(resultant)
|
||||
remote_checksum = self.get_checksum(tmp, dest, not directory_prepended, source=source)
|
||||
if isinstance(remote_checksum, dict):
|
||||
# Error from remote_checksum is a dict. Valid return is a str
|
||||
return remote_checksum
|
||||
|
||||
if local_checksum != remote_checksum:
|
||||
# if showing diffs, we need to get the remote value
|
||||
dest_contents = ''
|
||||
|
||||
# FIXME: still need to implement diff mechanism
|
||||
#if self.runner.diff:
|
||||
# # using persist_files to keep the temp directory around to avoid needing to grab another
|
||||
# dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % dest, task_vars=task_vars, persist_files=True)
|
||||
# if 'content' in dest_result.result:
|
||||
# dest_contents = dest_result.result['content']
|
||||
# if dest_result.result['encoding'] == 'base64':
|
||||
# dest_contents = base64.b64decode(dest_contents)
|
||||
# else:
|
||||
# raise Exception("unknown encoding, failed: %s" % dest_result.result)
|
||||
|
||||
xfered = self._transfer_data(self._shell.join_path(tmp, 'source'), resultant)
|
||||
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
self._remote_chmod('a+r', xfered, tmp)
|
||||
|
||||
# run the copy module
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=xfered,
|
||||
dest=dest,
|
||||
original_basename=os.path.basename(source),
|
||||
follow=True,
|
||||
),
|
||||
)
|
||||
|
||||
# FIXME: noop stuff needs to be sorted out
|
||||
#if self.runner.noop_on_check(task_vars):
|
||||
# return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=source, before=dest_contents, after=resultant))
|
||||
#else:
|
||||
# res = self.runner._execute_module(conn, tmp, 'copy', module_args_tmp, task_vars=task_vars, complex_args=complex_args)
|
||||
# if res.result.get('changed', False):
|
||||
# res.diff = dict(before=dest_contents, after=resultant)
|
||||
# return res
|
||||
|
||||
result = self._execute_module(module_name='copy', module_args=new_module_args)
|
||||
if result.get('changed', False):
|
||||
result['diff'] = dict(before=dest_contents, after=resultant)
|
||||
return result
|
||||
|
||||
else:
|
||||
# when running the file module based on the template data, we do
|
||||
# not want the source filename (the name of the template) to be used,
|
||||
# since this would mess up links, so we clear the src param and tell
|
||||
# the module to follow links. When doing that, we have to set
|
||||
# original_basename to the template just in case the dest is
|
||||
# a directory.
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=None,
|
||||
original_basename=os.path.basename(source),
|
||||
follow=True,
|
||||
),
|
||||
)
|
||||
|
||||
# FIXME: this may not be required anymore, as the checkmod params
|
||||
# should be in the regular module args?
|
||||
# be sure to task_vars the check mode param into the module args and
|
||||
# rely on the file module to report its changed status
|
||||
#if self.runner.noop_on_check(task_vars):
|
||||
# new_module_args['CHECKMODE'] = True
|
||||
|
||||
return self._execute_module(module_name='file', module_args=new_module_args)
|
||||
|
||||
114
lib/ansible/plugins/action/unarchive.py
Normal file
114
lib/ansible/plugins/action/unarchive.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2013, Dylan Martin <dmartin@seattlecentral.edu>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pipes
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
TRANSFERS_FILES = True
|
||||
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' handler for unarchive operations '''
|
||||
|
||||
source = self._task.args.get('src', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
copy = self._task.args.get('copy', True)
|
||||
creates = self._task.args.get('creates', None)
|
||||
|
||||
if source is None or dest is None:
|
||||
return dict(failed=True, msg="src (or content) and dest are required")
|
||||
|
||||
if not tmp:
|
||||
tmp = self._make_tmp_path()
|
||||
|
||||
if creates:
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
module_args_tmp = "path=%s" % creates
|
||||
result = self._execute_module(module_name='stat', module_args=dict(path=creates))
|
||||
stat = result.get('stat', None)
|
||||
if stat and stat.get('exists', False):
|
||||
return dict(skipped=True, msg=("skipped, since %s exists" % creates))
|
||||
|
||||
dest = self._remote_expand_user(dest, tmp) # CCTODO: Fix path for Windows hosts.
|
||||
source = os.path.expanduser(source)
|
||||
|
||||
if copy:
|
||||
# FIXME: the original file stuff needs to be reworked
|
||||
if '_original_file' in task_vars:
|
||||
source = self._loader.path_dwim_relative(task_vars['_original_file'], 'files', source)
|
||||
else:
|
||||
source = self._loader.path_dwim(source)
|
||||
|
||||
remote_checksum = self._remote_checksum(tmp, dest)
|
||||
if remote_checksum != '3':
|
||||
return dict(failed=True, msg="dest '%s' must be an existing dir" % dest)
|
||||
elif remote_checksum == '4':
|
||||
return dict(failed=True, msg="python isn't present on the system. Unable to compute checksum")
|
||||
|
||||
if copy:
|
||||
# transfer the file to a remote tmp location
|
||||
tmp_src = tmp + 'source'
|
||||
self._connection.put_file(source, tmp_src)
|
||||
|
||||
# handle diff mode client side
|
||||
# handle check mode client side
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if copy:
|
||||
if self._connection_info.become and self._connection_info.become_user != 'root':
|
||||
# FIXME: noop stuff needs to be reworked
|
||||
#if not self.runner.noop_on_check(task_vars):
|
||||
# self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp)
|
||||
self._remote_chmod(tmp, 'a+r', tmp_src)
|
||||
|
||||
# Build temporary module_args.
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
src=tmp_src,
|
||||
original_basename=os.path.basename(source),
|
||||
),
|
||||
)
|
||||
|
||||
# make sure checkmod is passed on correctly
|
||||
# FIXME: noop again, probably doesn't need to be done here anymore?
|
||||
#if self.runner.noop_on_check(task_vars):
|
||||
# new_module_args['CHECKMODE'] = True
|
||||
|
||||
else:
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
original_basename=os.path.basename(source),
|
||||
),
|
||||
)
|
||||
# make sure checkmod is passed on correctly
|
||||
# FIXME: noop again, probably doesn't need to be done here anymore?
|
||||
#if self.runner.noop_on_check(task_vars):
|
||||
# module_args += " CHECKMODE=True"
|
||||
|
||||
# execute the unarchive module now, with the updated args
|
||||
return self._execute_module(module_args=new_module_args)
|
||||
|
||||
62
lib/ansible/plugins/cache/__init__.py
vendored
Normal file
62
lib/ansible/plugins/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# (c) 2014, 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from collections import MutableMapping
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins import cache_loader
|
||||
|
||||
class FactCache(MutableMapping):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._plugin = cache_loader.get(C.CACHE_PLUGIN)
|
||||
if self._plugin is None:
|
||||
# FIXME: this should be an exception
|
||||
return
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self:
|
||||
raise KeyError
|
||||
return self._plugin.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._plugin.set(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
self._plugin.delete(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
return self._plugin.contains(key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._plugin.keys())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._plugin.keys())
|
||||
|
||||
def copy(self):
|
||||
""" Return a primitive copy of the keys and values from the cache. """
|
||||
return dict([(k, v) for (k, v) in self.iteritems()])
|
||||
|
||||
def keys(self):
|
||||
return self._plugin.keys()
|
||||
|
||||
def flush(self):
|
||||
""" Flush the fact cache of all keys. """
|
||||
self._plugin.flush()
|
||||
55
lib/ansible/plugins/cache/base.py
vendored
Normal file
55
lib/ansible/plugins/cache/base.py
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from six import add_metaclass
|
||||
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class BaseCacheModule:
|
||||
|
||||
@abstractmethod
|
||||
def get(self, key):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set(self, key, value):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def keys(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def contains(self, key):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, key):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def copy(self):
|
||||
pass
|
||||
193
lib/ansible/plugins/cache/memcached.py
vendored
Normal file
193
lib/ansible/plugins/cache/memcached.py
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import collections
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
from itertools import chain
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.cache.base import BaseCacheModule
|
||||
|
||||
try:
|
||||
import memcache
|
||||
except ImportError:
|
||||
print('python-memcached is required for the memcached fact cache')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ProxyClientPool(object):
|
||||
"""
|
||||
Memcached connection pooling for thread/fork safety. Inspired by py-redis
|
||||
connection pool.
|
||||
|
||||
Available connections are maintained in a deque and released in a FIFO manner.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.max_connections = kwargs.pop('max_connections', 1024)
|
||||
self.connection_args = args
|
||||
self.connection_kwargs = kwargs
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.pid = os.getpid()
|
||||
self._num_connections = 0
|
||||
self._available_connections = collections.deque(maxlen=self.max_connections)
|
||||
self._locked_connections = set()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _check_safe(self):
|
||||
if self.pid != os.getpid():
|
||||
with self._lock:
|
||||
if self.pid == os.getpid():
|
||||
# bail out - another thread already acquired the lock
|
||||
return
|
||||
self.disconnect_all()
|
||||
self.reset()
|
||||
|
||||
def get_connection(self):
|
||||
self._check_safe()
|
||||
try:
|
||||
connection = self._available_connections.popleft()
|
||||
except IndexError:
|
||||
connection = self.create_connection()
|
||||
self._locked_connections.add(connection)
|
||||
return connection
|
||||
|
||||
def create_connection(self):
|
||||
if self._num_connections >= self.max_connections:
|
||||
raise RuntimeError("Too many memcached connections")
|
||||
self._num_connections += 1
|
||||
return memcache.Client(*self.connection_args, **self.connection_kwargs)
|
||||
|
||||
def release_connection(self, connection):
|
||||
self._check_safe()
|
||||
self._locked_connections.remove(connection)
|
||||
self._available_connections.append(connection)
|
||||
|
||||
def disconnect_all(self):
|
||||
for conn in chain(self._available_connections, self._locked_connections):
|
||||
conn.disconnect_all()
|
||||
|
||||
def __getattr__(self, name):
|
||||
def wrapped(*args, **kwargs):
|
||||
return self._proxy_client(name, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
def _proxy_client(self, name, *args, **kwargs):
|
||||
conn = self.get_connection()
|
||||
|
||||
try:
|
||||
return getattr(conn, name)(*args, **kwargs)
|
||||
finally:
|
||||
self.release_connection(conn)
|
||||
|
||||
|
||||
class CacheModuleKeys(collections.MutableSet):
|
||||
"""
|
||||
A set subclass that keeps track of insertion time and persists
|
||||
the set in memcached.
|
||||
"""
|
||||
PREFIX = 'ansible_cache_keys'
|
||||
|
||||
def __init__(self, cache, *args, **kwargs):
|
||||
self._cache = cache
|
||||
self._keyset = dict(*args, **kwargs)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._keyset
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._keyset)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._keyset)
|
||||
|
||||
def add(self, key):
|
||||
self._keyset[key] = time.time()
|
||||
self._cache.set(self.PREFIX, self._keyset)
|
||||
|
||||
def discard(self, key):
|
||||
del self._keyset[key]
|
||||
self._cache.set(self.PREFIX, self._keyset)
|
||||
|
||||
def remove_by_timerange(self, s_min, s_max):
|
||||
for k in self._keyset.keys():
|
||||
t = self._keyset[k]
|
||||
if s_min < t < s_max:
|
||||
del self._keyset[k]
|
||||
self._cache.set(self.PREFIX, self._keyset)
|
||||
|
||||
|
||||
class CacheModule(BaseCacheModule):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if C.CACHE_PLUGIN_CONNECTION:
|
||||
connection = C.CACHE_PLUGIN_CONNECTION.split(',')
|
||||
else:
|
||||
connection = ['127.0.0.1:11211']
|
||||
|
||||
self._timeout = C.CACHE_PLUGIN_TIMEOUT
|
||||
self._prefix = C.CACHE_PLUGIN_PREFIX
|
||||
self._cache = ProxyClientPool(connection, debug=0)
|
||||
self._keys = CacheModuleKeys(self._cache, self._cache.get(CacheModuleKeys.PREFIX) or [])
|
||||
|
||||
def _make_key(self, key):
|
||||
return "{0}{1}".format(self._prefix, key)
|
||||
|
||||
def _expire_keys(self):
|
||||
if self._timeout > 0:
|
||||
expiry_age = time.time() - self._timeout
|
||||
self._keys.remove_by_timerange(0, expiry_age)
|
||||
|
||||
def get(self, key):
|
||||
value = self._cache.get(self._make_key(key))
|
||||
# guard against the key not being removed from the keyset;
|
||||
# this could happen in cases where the timeout value is changed
|
||||
# between invocations
|
||||
if value is None:
|
||||
self.delete(key)
|
||||
raise KeyError
|
||||
return value
|
||||
|
||||
def set(self, key, value):
|
||||
self._cache.set(self._make_key(key), value, time=self._timeout, min_compress_len=1)
|
||||
self._keys.add(key)
|
||||
|
||||
def keys(self):
|
||||
self._expire_keys()
|
||||
return list(iter(self._keys))
|
||||
|
||||
def contains(self, key):
|
||||
self._expire_keys()
|
||||
return key in self._keys
|
||||
|
||||
def delete(self, key):
|
||||
self._cache.delete(self._make_key(key))
|
||||
self._keys.discard(key)
|
||||
|
||||
def flush(self):
|
||||
for key in self.keys():
|
||||
self.delete(key)
|
||||
|
||||
def copy(self):
|
||||
return self._keys.copy()
|
||||
46
lib/ansible/plugins/cache/memory.py
vendored
Normal file
46
lib/ansible/plugins/cache/memory.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.cache.base import BaseCacheModule
|
||||
|
||||
class CacheModule(BaseCacheModule):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._cache = {}
|
||||
|
||||
def get(self, key):
|
||||
return self._cache.get(key)
|
||||
|
||||
def set(self, key, value):
|
||||
self._cache[key] = value
|
||||
|
||||
def keys(self):
|
||||
return self._cache.keys()
|
||||
|
||||
def contains(self, key):
|
||||
return key in self._cache
|
||||
|
||||
def delete(self, key):
|
||||
del self._cache[key]
|
||||
|
||||
def flush(self):
|
||||
self._cache = {}
|
||||
|
||||
def copy(self):
|
||||
return self._cache.copy()
|
||||
102
lib/ansible/plugins/cache/redis.py
vendored
Normal file
102
lib/ansible/plugins/cache/redis.py
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# FIXME: can we store these as something else before we ship it?
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.cache.base import BaseCacheModule
|
||||
|
||||
try:
|
||||
from redis import StrictRedis
|
||||
except ImportError:
|
||||
print("The 'redis' python module is required, 'pip install redis'")
|
||||
sys.exit(1)
|
||||
|
||||
class CacheModule(BaseCacheModule):
|
||||
"""
|
||||
A caching module backed by redis.
|
||||
|
||||
Keys are maintained in a zset with their score being the timestamp
|
||||
when they are inserted. This allows for the usage of 'zremrangebyscore'
|
||||
to expire keys. This mechanism is used or a pattern matched 'scan' for
|
||||
performance.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
if C.CACHE_PLUGIN_CONNECTION:
|
||||
connection = C.CACHE_PLUGIN_CONNECTION.split(':')
|
||||
else:
|
||||
connection = []
|
||||
|
||||
self._timeout = float(C.CACHE_PLUGIN_TIMEOUT)
|
||||
self._prefix = C.CACHE_PLUGIN_PREFIX
|
||||
self._cache = StrictRedis(*connection)
|
||||
self._keys_set = 'ansible_cache_keys'
|
||||
|
||||
def _make_key(self, key):
|
||||
return self._prefix + key
|
||||
|
||||
def get(self, key):
|
||||
value = self._cache.get(self._make_key(key))
|
||||
# guard against the key not being removed from the zset;
|
||||
# this could happen in cases where the timeout value is changed
|
||||
# between invocations
|
||||
if value is None:
|
||||
self.delete(key)
|
||||
raise KeyError
|
||||
return json.loads(value)
|
||||
|
||||
def set(self, key, value):
|
||||
value2 = json.dumps(value)
|
||||
if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
|
||||
self._cache.setex(self._make_key(key), int(self._timeout), value2)
|
||||
else:
|
||||
self._cache.set(self._make_key(key), value2)
|
||||
|
||||
self._cache.zadd(self._keys_set, time.time(), key)
|
||||
|
||||
def _expire_keys(self):
|
||||
if self._timeout > 0:
|
||||
expiry_age = time.time() - self._timeout
|
||||
self._cache.zremrangebyscore(self._keys_set, 0, expiry_age)
|
||||
|
||||
def keys(self):
|
||||
self._expire_keys()
|
||||
return self._cache.zrange(self._keys_set, 0, -1)
|
||||
|
||||
def contains(self, key):
|
||||
self._expire_keys()
|
||||
return (self._cache.zrank(self._keys_set, key) >= 0)
|
||||
|
||||
def delete(self, key):
|
||||
self._cache.delete(self._make_key(key))
|
||||
self._cache.zrem(self._keys_set, key)
|
||||
|
||||
def flush(self):
|
||||
for key in self.keys():
|
||||
self.delete(key)
|
||||
|
||||
def copy(self):
|
||||
# FIXME: there is probably a better way to do this in redis
|
||||
ret = dict()
|
||||
for key in self.keys():
|
||||
ret[key] = self.get(key)
|
||||
return ret
|
||||
104
lib/ansible/plugins/callback/__init__.py
Normal file
104
lib/ansible/plugins/callback/__init__.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
#from ansible.utils.display import Display
|
||||
|
||||
__all__ = ["CallbackBase"]
|
||||
|
||||
class CallbackBase:
|
||||
|
||||
'''
|
||||
This is a base ansible callback class that does nothing. New callbacks should
|
||||
use this class as a base and override any callback methods they wish to execute
|
||||
custom actions.
|
||||
'''
|
||||
|
||||
# FIXME: the list of functions here needs to be updated once we have
|
||||
# finalized the list of callback methods used in the default callback
|
||||
|
||||
def __init__(self, display):
|
||||
self._display = display
|
||||
|
||||
def set_connection_info(self, conn_info):
|
||||
# FIXME: this is a temporary hack, as the connection info object
|
||||
# should be created early and passed down through objects
|
||||
self._display._verbosity = conn_info.verbosity
|
||||
|
||||
def on_any(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def runner_on_failed(self, host, res, ignore_errors=False):
|
||||
pass
|
||||
|
||||
def runner_on_ok(self, host, res):
|
||||
pass
|
||||
|
||||
def runner_on_skipped(self, host, item=None):
|
||||
pass
|
||||
|
||||
def runner_on_unreachable(self, host, res):
|
||||
pass
|
||||
|
||||
def runner_on_no_hosts(self):
|
||||
pass
|
||||
|
||||
def runner_on_async_poll(self, host, res, jid, clock):
|
||||
pass
|
||||
|
||||
def runner_on_async_ok(self, host, res, jid):
|
||||
pass
|
||||
|
||||
def runner_on_async_failed(self, host, res, jid):
|
||||
pass
|
||||
|
||||
def playbook_on_start(self):
|
||||
pass
|
||||
|
||||
def playbook_on_notify(self, host, handler):
|
||||
pass
|
||||
|
||||
def playbook_on_no_hosts_matched(self):
|
||||
pass
|
||||
|
||||
def playbook_on_no_hosts_remaining(self):
|
||||
pass
|
||||
|
||||
def playbook_on_task_start(self, name, is_conditional):
|
||||
pass
|
||||
|
||||
def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
|
||||
pass
|
||||
|
||||
def playbook_on_setup(self):
|
||||
pass
|
||||
|
||||
def playbook_on_import_for_host(self, host, imported_file):
|
||||
pass
|
||||
|
||||
def playbook_on_not_import_for_host(self, host, missing_file):
|
||||
pass
|
||||
|
||||
def playbook_on_play_start(self, name):
|
||||
pass
|
||||
|
||||
def playbook_on_stats(self, stats):
|
||||
pass
|
||||
|
||||
136
lib/ansible/plugins/callback/default.py
Normal file
136
lib/ansible/plugins/callback/default.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
|
||||
class CallbackModule(CallbackBase):
|
||||
|
||||
'''
|
||||
This is the default callback interface, which simply prints messages
|
||||
to stdout when new callback events are received.
|
||||
'''
|
||||
|
||||
CALLBACK_VERSION = 2.0
|
||||
CALLBACK_TYPE = 'stdout'
|
||||
|
||||
def v2_on_any(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
if 'exception' in result._result and self._display.verbosity < 3:
|
||||
del result._result['exception']
|
||||
self._display.display("fatal: [%s]: FAILED! => %s" % (result._host.get_name(), json.dumps(result._result, ensure_ascii=False)), color='red')
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
|
||||
if result._task.action == 'include':
|
||||
msg = 'included: %s for %s' % (result._task.args.get('_raw_params'), result._host.name)
|
||||
color = 'cyan'
|
||||
elif result._result.get('changed', False):
|
||||
msg = "changed: [%s]" % result._host.get_name()
|
||||
color = 'yellow'
|
||||
else:
|
||||
msg = "ok: [%s]" % result._host.get_name()
|
||||
color = 'green'
|
||||
|
||||
if (self._display._verbosity > 0 or 'verbose_always' in result._result) and result._task.action not in ('setup', 'include'):
|
||||
indent = None
|
||||
if 'verbose_always' in result._result:
|
||||
indent = 4
|
||||
del result._result['verbose_always']
|
||||
msg += " => %s" % json.dumps(result._result, indent=indent, ensure_ascii=False)
|
||||
self._display.display(msg, color=color)
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
msg = "skipping: [%s]" % result._host.get_name()
|
||||
if self._display._verbosity > 0 or 'verbose_always' in result._result:
|
||||
indent = None
|
||||
if 'verbose_always' in result._result:
|
||||
indent = 4
|
||||
del result._result['verbose_always']
|
||||
msg += " => %s" % json.dumps(result._result, indent=indent, ensure_ascii=False)
|
||||
self._display.display(msg, color='cyan')
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
self._display.display("fatal: [%s]: UNREACHABLE! => %s" % (result._host.get_name(), result._result), color='red')
|
||||
|
||||
def v2_runner_on_no_hosts(self, task):
|
||||
pass
|
||||
|
||||
def v2_runner_on_async_poll(self, result):
|
||||
pass
|
||||
|
||||
def v2_runner_on_async_ok(self, result):
|
||||
pass
|
||||
|
||||
def v2_runner_on_async_failed(self, result):
|
||||
pass
|
||||
|
||||
def v2_runner_on_file_diff(self, result, diff):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_start(self):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_notify(self, result, handler):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_no_hosts_matched(self):
|
||||
self._display.display("skipping: no hosts matched", color='cyan')
|
||||
|
||||
def v2_playbook_on_no_hosts_remaining(self):
|
||||
self._display.banner("NO MORE HOSTS LEFT")
|
||||
|
||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||
self._display.banner("TASK [%s]" % task.get_name().strip())
|
||||
|
||||
def v2_playbook_on_cleanup_task_start(self, task):
|
||||
self._display.banner("CLEANUP TASK [%s]" % task.get_name().strip())
|
||||
|
||||
def v2_playbook_on_handler_task_start(self, task):
|
||||
self._display.banner("RUNNING HANDLER [%s]" % task.get_name().strip())
|
||||
|
||||
def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_setup(self):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_import_for_host(self, result, imported_file):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_play_start(self, play):
|
||||
name = play.get_name().strip()
|
||||
if not name:
|
||||
msg = "PLAY"
|
||||
else:
|
||||
msg = "PLAY [%s]" % name
|
||||
|
||||
self._display.banner(name)
|
||||
|
||||
def v2_playbook_on_stats(self, stats):
|
||||
pass
|
||||
|
||||
104
lib/ansible/plugins/callback/minimal.py
Normal file
104
lib/ansible/plugins/callback/minimal.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
|
||||
|
||||
class CallbackModule(CallbackBase):
|
||||
|
||||
'''
|
||||
This is the default callback interface, which simply prints messages
|
||||
to stdout when new callback events are received.
|
||||
'''
|
||||
|
||||
CALLBACK_VERSION = 2.0
|
||||
CALLBACK_TYPE = 'stdout'
|
||||
|
||||
def v2_on_any(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
if 'exception' in result._result and self._display.verbosity < 3:
|
||||
del result._result['exception']
|
||||
self._display.display("%s | FAILED! => %s" % (result._host.get_name(), result._result), color='red')
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
self._display.display("%s | SUCCESS => %s" % (result._host.get_name(), json.dumps(result._result, indent=4)), color='green')
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
pass
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
self._display.display("%s | UNREACHABLE!" % result._host.get_name(), color='yellow')
|
||||
|
||||
def v2_runner_on_no_hosts(self, task):
|
||||
pass
|
||||
|
||||
def v2_runner_on_async_poll(self, host, res, jid, clock):
|
||||
pass
|
||||
|
||||
def v2_runner_on_async_ok(self, host, res, jid):
|
||||
pass
|
||||
|
||||
def v2_runner_on_async_failed(self, host, res, jid):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_start(self):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_notify(self, host, handler):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_no_hosts_matched(self):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_no_hosts_remaining(self):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_cleanup_task_start(self, task):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_handler_task_start(self, task):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_setup(self):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_import_for_host(self, result, imported_file):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_play_start(self, play):
|
||||
pass
|
||||
|
||||
def v2_playbook_on_stats(self, stats):
|
||||
pass
|
||||
|
||||
95
lib/ansible/plugins/connections/__init__.py
Normal file
95
lib/ansible/plugins/connections/__init__.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
|
||||
from six import add_metaclass
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
# FIXME: this object should be created upfront and passed through
|
||||
# the entire chain of calls to here, as there are other things
|
||||
# which may want to output display/logs too
|
||||
from ansible.utils.display import Display
|
||||
|
||||
__all__ = ['ConnectionBase']
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class ConnectionBase:
|
||||
'''
|
||||
A base class for connections to contain common code.
|
||||
'''
|
||||
|
||||
has_pipelining = False
|
||||
become_methods = C.BECOME_METHODS
|
||||
|
||||
def __init__(self, connection_info, new_stdin, *args, **kwargs):
|
||||
# All these hasattrs allow subclasses to override these parameters
|
||||
if not hasattr(self, '_connection_info'):
|
||||
self._connection_info = connection_info
|
||||
if not hasattr(self, '_new_stdin'):
|
||||
self._new_stdin = new_stdin
|
||||
if not hasattr(self, '_display'):
|
||||
self._display = Display(verbosity=connection_info.verbosity)
|
||||
if not hasattr(self, '_connected'):
|
||||
self._connected = False
|
||||
|
||||
self._connect()
|
||||
|
||||
def _become_method_supported(self, become_method):
|
||||
''' Checks if the current class supports this privilege escalation method '''
|
||||
|
||||
if become_method in self.__class__.become_methods:
|
||||
return True
|
||||
|
||||
raise AnsibleError("Internal Error: this connection module does not support running commands via %s" % become_method)
|
||||
|
||||
@abstractproperty
|
||||
def transport(self):
|
||||
"""String used to identify this Connection class from other classes"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _connect(self):
|
||||
"""Connect to the host we've been initialized with"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def exec_command(self, cmd, tmp_path, executable=None, in_data=None):
|
||||
"""Run a command on the remote host"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def put_file(self, in_path, out_path):
|
||||
"""Transfer a file from local to remote"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fetch_file(self, in_path, out_path):
|
||||
"""Fetch a file from remote to local"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
"""Terminate the connection"""
|
||||
pass
|
||||
381
lib/ansible/plugins/connections/accelerate.py
Normal file
381
lib/ansible/plugins/connections/accelerate.py
Normal file
@@ -0,0 +1,381 @@
|
||||
# (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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
from ansible.callbacks import vvv, vvvv
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||
from . import ConnectionBase
|
||||
from .ssh import Connection as SSHConnection
|
||||
from .paramiko_ssh import Connection as ParamikoConnection
|
||||
from ansible import utils
|
||||
from ansible import constants
|
||||
|
||||
# the chunk size to read and send, assuming mtu 1500 and
|
||||
# leaving room for base64 (+33%) encoding and header (8 bytes)
|
||||
# ((1400-8)/4)*3) = 1044
|
||||
# which leaves room for the TCP/IP header. We set this to a
|
||||
# multiple of the value to speed up file reads.
|
||||
CHUNK_SIZE=1044*20
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' raw socket accelerated connection '''
|
||||
|
||||
def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
|
||||
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.context = None
|
||||
self.conn = None
|
||||
self.user = user
|
||||
self.key = utils.key_for_hostname(host)
|
||||
self.port = port[0]
|
||||
self.accport = port[1]
|
||||
self.is_connected = False
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=['sudo']
|
||||
|
||||
if not self.port:
|
||||
self.port = constants.DEFAULT_REMOTE_PORT
|
||||
elif not isinstance(self.port, int):
|
||||
self.port = int(self.port)
|
||||
|
||||
if not self.accport:
|
||||
self.accport = constants.ACCELERATE_PORT
|
||||
elif not isinstance(self.accport, int):
|
||||
self.accport = int(self.accport)
|
||||
|
||||
if self.runner.original_transport == "paramiko":
|
||||
self.ssh = ParamikoConnection(
|
||||
runner=self.runner,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=password,
|
||||
private_key_file=private_key_file
|
||||
)
|
||||
else:
|
||||
self.ssh = SSHConnection(
|
||||
runner=self.runner,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=password,
|
||||
private_key_file=private_key_file
|
||||
)
|
||||
|
||||
if not getattr(self.ssh, 'shell', None):
|
||||
self.ssh.shell = utils.plugins.shell_loader.get('sh')
|
||||
|
||||
# attempt to work around shared-memory funness
|
||||
if getattr(self.runner, 'aes_keys', None):
|
||||
utils.AES_KEYS = self.runner.aes_keys
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
"""String used to identify this Connection class from other classes"""
|
||||
return 'accelerate'
|
||||
|
||||
def _execute_accelerate_module(self):
|
||||
args = "password=%s port=%s minutes=%d debug=%d ipv6=%s" % (
|
||||
base64.b64encode(self.key.__str__()),
|
||||
str(self.accport),
|
||||
constants.ACCELERATE_DAEMON_TIMEOUT,
|
||||
int(utils.VERBOSITY),
|
||||
self.runner.accelerate_ipv6,
|
||||
)
|
||||
if constants.ACCELERATE_MULTI_KEY:
|
||||
args += " multi_key=yes"
|
||||
inject = dict(password=self.key)
|
||||
if getattr(self.runner, 'accelerate_inventory_host', False):
|
||||
inject = utils.combine_vars(inject, self.runner.inventory.get_variables(self.runner.accelerate_inventory_host))
|
||||
else:
|
||||
inject = utils.combine_vars(inject, self.runner.inventory.get_variables(self.host))
|
||||
vvvv("attempting to start up the accelerate daemon...")
|
||||
self.ssh.connect()
|
||||
tmp_path = self.runner._make_tmp_path(self.ssh)
|
||||
return self.runner._execute_module(self.ssh, tmp_path, 'accelerate', args, inject=inject)
|
||||
|
||||
def connect(self, allow_ssh=True):
|
||||
''' activates the connection object '''
|
||||
|
||||
try:
|
||||
if not self.is_connected:
|
||||
wrong_user = False
|
||||
tries = 3
|
||||
self.conn = socket.socket()
|
||||
self.conn.settimeout(constants.ACCELERATE_CONNECT_TIMEOUT)
|
||||
vvvv("attempting connection to %s via the accelerated port %d" % (self.host,self.accport))
|
||||
while tries > 0:
|
||||
try:
|
||||
self.conn.connect((self.host,self.accport))
|
||||
break
|
||||
except socket.error:
|
||||
vvvv("connection to %s failed, retrying..." % self.host)
|
||||
time.sleep(0.1)
|
||||
tries -= 1
|
||||
if tries == 0:
|
||||
vvv("Could not connect via the accelerated connection, exceeded # of tries")
|
||||
raise AnsibleError("FAILED")
|
||||
elif wrong_user:
|
||||
vvv("Restarting daemon with a different remote_user")
|
||||
raise AnsibleError("WRONG_USER")
|
||||
|
||||
self.conn.settimeout(constants.ACCELERATE_TIMEOUT)
|
||||
if not self.validate_user():
|
||||
# the accelerated daemon was started with a
|
||||
# different remote_user. The above command
|
||||
# should have caused the accelerate daemon to
|
||||
# shutdown, so we'll reconnect.
|
||||
wrong_user = True
|
||||
|
||||
except AnsibleError as e:
|
||||
if allow_ssh:
|
||||
if "WRONG_USER" in e:
|
||||
vvv("Switching users, waiting for the daemon on %s to shutdown completely..." % self.host)
|
||||
time.sleep(5)
|
||||
vvv("Falling back to ssh to startup accelerated mode")
|
||||
res = self._execute_accelerate_module()
|
||||
if not res.is_successful():
|
||||
raise AnsibleError("Failed to launch the accelerated daemon on %s (reason: %s)" % (self.host,res.result.get('msg')))
|
||||
return self.connect(allow_ssh=False)
|
||||
else:
|
||||
raise AnsibleError("Failed to connect to %s:%s" % (self.host,self.accport))
|
||||
self.is_connected = True
|
||||
return self
|
||||
|
||||
def send_data(self, data):
|
||||
packed_len = struct.pack('!Q',len(data))
|
||||
return self.conn.sendall(packed_len + data)
|
||||
|
||||
def recv_data(self):
|
||||
header_len = 8 # size of a packed unsigned long long
|
||||
data = b""
|
||||
try:
|
||||
vvvv("%s: in recv_data(), waiting for the header" % self.host)
|
||||
while len(data) < header_len:
|
||||
d = self.conn.recv(header_len - len(data))
|
||||
if not d:
|
||||
vvvv("%s: received nothing, bailing out" % self.host)
|
||||
return None
|
||||
data += d
|
||||
vvvv("%s: got the header, unpacking" % self.host)
|
||||
data_len = struct.unpack('!Q',data[:header_len])[0]
|
||||
data = data[header_len:]
|
||||
vvvv("%s: data received so far (expecting %d): %d" % (self.host,data_len,len(data)))
|
||||
while len(data) < data_len:
|
||||
d = self.conn.recv(data_len - len(data))
|
||||
if not d:
|
||||
vvvv("%s: received nothing, bailing out" % self.host)
|
||||
return None
|
||||
vvvv("%s: received %d bytes" % (self.host, len(d)))
|
||||
data += d
|
||||
vvvv("%s: received all of the data, returning" % self.host)
|
||||
return data
|
||||
except socket.timeout:
|
||||
raise AnsibleError("timed out while waiting to receive data")
|
||||
|
||||
def validate_user(self):
|
||||
'''
|
||||
Checks the remote uid of the accelerated daemon vs. the
|
||||
one specified for this play and will cause the accel
|
||||
daemon to exit if they don't match
|
||||
'''
|
||||
|
||||
vvvv("%s: sending request for validate_user" % self.host)
|
||||
data = dict(
|
||||
mode='validate_user',
|
||||
username=self.user,
|
||||
)
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("Failed to send command to %s" % self.host)
|
||||
|
||||
vvvv("%s: waiting for validate_user response" % self.host)
|
||||
while True:
|
||||
# we loop here while waiting for the response, because a
|
||||
# long running command may cause us to receive keepalive packets
|
||||
# ({"pong":"true"}) rather than the response we want.
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
if "pong" in response:
|
||||
# it's a keepalive, go back to waiting
|
||||
vvvv("%s: received a keepalive packet" % self.host)
|
||||
continue
|
||||
else:
|
||||
vvvv("%s: received the validate_user response: %s" % (self.host, response))
|
||||
break
|
||||
|
||||
if response.get('failed'):
|
||||
return False
|
||||
else:
|
||||
return response.get('rc') == 0
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
if executable == "":
|
||||
executable = constants.DEFAULT_EXECUTABLE
|
||||
|
||||
if self.runner.become and sudoable:
|
||||
cmd, prompt, success_key = utils.make_become_cmd(cmd, become_user, executable, self.runner.become_method, '', self.runner.become_exe)
|
||||
|
||||
vvv("EXEC COMMAND %s" % cmd)
|
||||
|
||||
data = dict(
|
||||
mode='command',
|
||||
cmd=cmd,
|
||||
tmp_path=tmp_path,
|
||||
executable=executable,
|
||||
)
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("Failed to send command to %s" % self.host)
|
||||
|
||||
while True:
|
||||
# we loop here while waiting for the response, because a
|
||||
# long running command may cause us to receive keepalive packets
|
||||
# ({"pong":"true"}) rather than the response we want.
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
if "pong" in response:
|
||||
# it's a keepalive, go back to waiting
|
||||
vvvv("%s: received a keepalive packet" % self.host)
|
||||
continue
|
||||
else:
|
||||
vvvv("%s: received the response" % self.host)
|
||||
break
|
||||
|
||||
return (response.get('rc',None), '', response.get('stdout',''), response.get('stderr',''))
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
|
||||
''' transfer a file from local to remote '''
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
||||
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
|
||||
fd = file(in_path, 'rb')
|
||||
fstat = os.stat(in_path)
|
||||
try:
|
||||
vvv("PUT file is %d bytes" % fstat.st_size)
|
||||
last = False
|
||||
while fd.tell() <= fstat.st_size and not last:
|
||||
vvvv("file position currently %ld, file size is %ld" % (fd.tell(), fstat.st_size))
|
||||
data = fd.read(CHUNK_SIZE)
|
||||
if fd.tell() >= fstat.st_size:
|
||||
last = True
|
||||
data = dict(mode='put', data=base64.b64encode(data), out_path=out_path, last=last)
|
||||
if self.runner.become:
|
||||
data['user'] = self.runner.become_user
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("failed to send the file to %s" % self.host)
|
||||
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
|
||||
if response.get('failed',False):
|
||||
raise AnsibleError("failed to put the file in the requested location")
|
||||
finally:
|
||||
fd.close()
|
||||
vvvv("waiting for final response after PUT")
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
|
||||
if response.get('failed',False):
|
||||
raise AnsibleError("failed to put the file in the requested location")
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' save a remote file to the specified path '''
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
||||
|
||||
data = dict(mode='fetch', in_path=in_path)
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("failed to initiate the file fetch with %s" % self.host)
|
||||
|
||||
fh = open(out_path, "w")
|
||||
try:
|
||||
bytes = 0
|
||||
while True:
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
if response.get('failed', False):
|
||||
raise AnsibleError("Error during file fetch, aborting")
|
||||
out = base64.b64decode(response['data'])
|
||||
fh.write(out)
|
||||
bytes += len(out)
|
||||
# send an empty response back to signify we
|
||||
# received the last chunk without errors
|
||||
data = utils.jsonify(dict())
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("failed to send ack during file fetch")
|
||||
if response.get('last', False):
|
||||
break
|
||||
finally:
|
||||
# we don't currently care about this final response,
|
||||
# we just receive it and drop it. It may be used at some
|
||||
# point in the future or we may just have the put/fetch
|
||||
# operations not send back a final response at all
|
||||
response = self.recv_data()
|
||||
vvv("FETCH wrote %d bytes to %s" % (bytes, out_path))
|
||||
fh.close()
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection '''
|
||||
# Be a good citizen
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
134
lib/ansible/plugins/connections/chroot.py
Normal file
134
lib/ansible/plugins/connections/chroot.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2013, Maykel Moya <mmoya@speedyrails.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import traceback
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
class Connection(object):
|
||||
''' Local chroot based connections '''
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.chroot = host
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
if os.geteuid() != 0:
|
||||
raise errors.AnsibleError("chroot connection requires running as root")
|
||||
|
||||
# we're running as root on the local system so do some
|
||||
# trivial checks for ensuring 'host' is actually a chroot'able dir
|
||||
if not os.path.isdir(self.chroot):
|
||||
raise errors.AnsibleError("%s is not a directory" % self.chroot)
|
||||
|
||||
chrootsh = os.path.join(self.chroot, 'bin/sh')
|
||||
if not utils.is_executable(chrootsh):
|
||||
raise errors.AnsibleError("%s does not look like a chrootable dir (/bin/sh missing)" % self.chroot)
|
||||
|
||||
self.chroot_cmd = distutils.spawn.find_executable('chroot')
|
||||
if not self.chroot_cmd:
|
||||
raise errors.AnsibleError("chroot command not found in PATH")
|
||||
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the chroot; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL CHROOT DIR", host=self.chroot)
|
||||
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the chroot '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We enter chroot as root so we ignore privlege escalation?
|
||||
|
||||
if executable:
|
||||
local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
|
||||
else:
|
||||
local_cmd = '%s "%s" %s' % (self.chroot_cmd, self.chroot, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.chroot)
|
||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to chroot '''
|
||||
|
||||
if not out_path.startswith(os.path.sep):
|
||||
out_path = os.path.join(os.path.sep, out_path)
|
||||
normpath = os.path.normpath(out_path)
|
||||
out_path = os.path.join(self.chroot, normpath[1:])
|
||||
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.chroot)
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
try:
|
||||
shutil.copyfile(in_path, out_path)
|
||||
except shutil.Error:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from chroot to local '''
|
||||
|
||||
if not in_path.startswith(os.path.sep):
|
||||
in_path = os.path.join(os.path.sep, in_path)
|
||||
normpath = os.path.normpath(in_path)
|
||||
in_path = os.path.join(self.chroot, normpath[1:])
|
||||
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.chroot)
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
try:
|
||||
shutil.copyfile(in_path, out_path)
|
||||
except shutil.Error:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
99
lib/ansible/plugins/connections/funcd.py
Normal file
99
lib/ansible/plugins/connections/funcd.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# ---
|
||||
# The func transport permit to use ansible over func. For people who have already setup
|
||||
# func and that wish to play with ansible, this permit to move gradually to ansible
|
||||
# without having to redo completely the setup of the network.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
HAVE_FUNC=False
|
||||
try:
|
||||
import func.overlord.client as fc
|
||||
HAVE_FUNC=True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import os
|
||||
from ansible.callbacks import vvv
|
||||
from ansible import errors
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
class Connection(object):
|
||||
''' Func-based connections '''
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.has_pipelining = False
|
||||
# port is unused, this go on func
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
if not HAVE_FUNC:
|
||||
raise errors.AnsibleError("func is not installed")
|
||||
|
||||
self.client = fc.Client(self.host)
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False,
|
||||
executable='/bin/sh', in_data=None):
|
||||
''' run a command on the remote minion '''
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# totally ignores privlege escalation
|
||||
vvv("EXEC %s" % (cmd), host=self.host)
|
||||
p = self.client.command.run(cmd)[self.host]
|
||||
return (p[0], '', p[1], p[2])
|
||||
|
||||
def _normalize_path(self, path, prefix):
|
||||
if not path.startswith(os.path.sep):
|
||||
path = os.path.join(os.path.sep, path)
|
||||
normpath = os.path.normpath(path)
|
||||
return os.path.join(prefix, normpath[1:])
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
|
||||
out_path = self._normalize_path(out_path, '/')
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
||||
self.client.local.copyfile.send(in_path, out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from remote to local '''
|
||||
|
||||
in_path = self._normalize_path(in_path, '/')
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
||||
# need to use a tmp dir due to difference of semantic for getfile
|
||||
# ( who take a # directory as destination) and fetch_file, who
|
||||
# take a file directly
|
||||
tmpdir = tempfile.mkdtemp(prefix="func_ansible")
|
||||
self.client.local.getfile.get(in_path, tmpdir)
|
||||
shutil.move(os.path.join(tmpdir, self.host, os.path.basename(in_path)),
|
||||
out_path)
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
155
lib/ansible/plugins/connections/jail.py
Normal file
155
lib/ansible/plugins/connections/jail.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import traceback
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from ansible import errors
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
class Connection(object):
|
||||
''' Local chroot based connections '''
|
||||
|
||||
def _search_executable(self, executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
||||
return cmd
|
||||
|
||||
def list_jails(self):
|
||||
p = subprocess.Popen([self.jls_cmd, '-q', 'name'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
return stdout.split()
|
||||
|
||||
def get_jail_path(self):
|
||||
p = subprocess.Popen([self.jls_cmd, '-j', self.jail, '-q', 'path'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
# remove \n
|
||||
return stdout[:-1]
|
||||
|
||||
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.jail = host
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
if os.geteuid() != 0:
|
||||
raise errors.AnsibleError("jail connection requires running as root")
|
||||
|
||||
self.jls_cmd = self._search_executable('jls')
|
||||
self.jexec_cmd = self._search_executable('jexec')
|
||||
|
||||
if not self.jail in self.list_jails():
|
||||
raise errors.AnsibleError("incorrect jail name %s" % self.jail)
|
||||
|
||||
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the chroot; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL CHROOT DIR", host=self.jail)
|
||||
|
||||
return self
|
||||
|
||||
# a modifier
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
local_cmd = [self.jexec_cmd, self.jail, executable, '-c', cmd]
|
||||
else:
|
||||
local_cmd = '%s "%s" %s' % (self.jexec_cmd, self.jail, cmd)
|
||||
return local_cmd
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the chroot '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# Ignores privilege escalation
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.jail)
|
||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def _normalize_path(self, path, prefix):
|
||||
if not path.startswith(os.path.sep):
|
||||
path = os.path.join(os.path.sep, path)
|
||||
normpath = os.path.normpath(path)
|
||||
return os.path.join(prefix, normpath[1:])
|
||||
|
||||
def _copy_file(self, in_path, out_path):
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
try:
|
||||
shutil.copyfile(in_path, out_path)
|
||||
except shutil.Error:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to chroot '''
|
||||
|
||||
out_path = self._normalize_path(out_path, self.get_jail_path())
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.jail)
|
||||
|
||||
self._copy_file(in_path, out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from chroot to local '''
|
||||
|
||||
in_path = self._normalize_path(in_path, self.get_jail_path())
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.jail)
|
||||
|
||||
self._copy_file(in_path, out_path)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
131
lib/ansible/plugins/connections/libvirt_lxc.py
Normal file
131
lib/ansible/plugins/connections/libvirt_lxc.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import os
|
||||
import subprocess
|
||||
from ansible import errors
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
class Connection(object):
|
||||
''' Local lxc based connections '''
|
||||
|
||||
def _search_executable(self, executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
||||
return cmd
|
||||
|
||||
def _check_domain(self, domain):
|
||||
p = subprocess.Popen([self.cmd, '-q', '-c', 'lxc:///', 'dominfo', domain],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
if p.returncode:
|
||||
raise errors.AnsibleError("%s is not a lxc defined in libvirt" % domain)
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.lxc = host
|
||||
|
||||
self.cmd = self._search_executable('virsh')
|
||||
|
||||
self._check_domain(host)
|
||||
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the lxc; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL LXC DIR", host=self.lxc)
|
||||
|
||||
return self
|
||||
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', executable , '-c', cmd]
|
||||
else:
|
||||
local_cmd = '%s -q -c lxc:/// lxc-enter-namespace %s -- %s' % (self.cmd, self.lxc, cmd)
|
||||
return local_cmd
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the chroot '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We ignore privilege escalation!
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def _normalize_path(self, path, prefix):
|
||||
if not path.startswith(os.path.sep):
|
||||
path = os.path.join(os.path.sep, path)
|
||||
normpath = os.path.normpath(path)
|
||||
return os.path.join(prefix, normpath[1:])
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to lxc '''
|
||||
|
||||
out_path = self._normalize_path(out_path, '/')
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.lxc)
|
||||
|
||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', '/bin/tee', out_path]
|
||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
||||
|
||||
p = subprocess.Popen(local_cmd, cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate(open(in_path,'rb').read())
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from lxc to local '''
|
||||
|
||||
in_path = self._normalize_path(in_path, '/')
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.lxc)
|
||||
|
||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', '/bin/cat', in_path]
|
||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
||||
|
||||
p = subprocess.Popen(local_cmd, cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
open(out_path,'wb').write(stdout)
|
||||
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
132
lib/ansible/plugins/connections/local.py
Normal file
132
lib/ansible/plugins/connections/local.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
#import select
|
||||
#import fcntl
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' Local based connections '''
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object '''
|
||||
return 'local'
|
||||
|
||||
def _connect(self, port=None):
|
||||
''' connect to the local host; nothing to do here '''
|
||||
|
||||
if not self._connected:
|
||||
self._display.vvv("ESTABLISH LOCAL CONNECTION FOR USER: {0}".format(self._connection_info.remote_user, host=self._connection_info.remote_addr))
|
||||
self._connected = True
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the local host '''
|
||||
|
||||
debug("in local.exec_command()")
|
||||
# su requires to be run from a terminal, and therefore isn't supported here (yet?)
|
||||
#if self._connection_info.su:
|
||||
# raise AnsibleError("Internal Error: this module does not support running commands via su")
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
executable = executable.split()[0] if executable else None
|
||||
|
||||
self._display.vvv("{0} EXEC {1}".format(self._connection_info.remote_addr, cmd))
|
||||
# FIXME: cwd= needs to be set to the basedir of the playbook
|
||||
debug("opening command with Popen()")
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
shell=isinstance(cmd, basestring),
|
||||
executable=executable, #cwd=...
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
debug("done running command with Popen()")
|
||||
|
||||
# FIXME: more su/sudo stuff
|
||||
#if self.runner.sudo and sudoable and self.runner.sudo_pass:
|
||||
# fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
||||
# fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
# fcntl.fcntl(p.stderr, fcntl.F_SETFL,
|
||||
# fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
# sudo_output = ''
|
||||
# while not sudo_output.endswith(prompt) and success_key not in sudo_output:
|
||||
# rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
|
||||
# [p.stdout, p.stderr], self.runner.timeout)
|
||||
# if p.stdout in rfd:
|
||||
# chunk = p.stdout.read()
|
||||
# elif p.stderr in rfd:
|
||||
# chunk = p.stderr.read()
|
||||
# else:
|
||||
# stdout, stderr = p.communicate()
|
||||
# raise AnsibleError('timeout waiting for sudo password prompt:\n' + sudo_output)
|
||||
# if not chunk:
|
||||
# stdout, stderr = p.communicate()
|
||||
# raise AnsibleError('sudo output closed while waiting for password prompt:\n' + sudo_output)
|
||||
# sudo_output += chunk
|
||||
# if success_key not in sudo_output:
|
||||
# p.stdin.write(self.runner.sudo_pass + '\n')
|
||||
# fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
# fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
|
||||
debug("getting output with communicate()")
|
||||
stdout, stderr = p.communicate()
|
||||
debug("done communicating")
|
||||
|
||||
debug("done with local.exec_command()")
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to local '''
|
||||
|
||||
#vvv("PUT {0} TO {1}".format(in_path, out_path), host=self.host)
|
||||
self._display.vvv("{0} PUT {1} TO {2}".format(self._connection_info.remote_addr, in_path, out_path))
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: {0}".format(in_path))
|
||||
try:
|
||||
shutil.copyfile(in_path, out_path)
|
||||
except shutil.Error:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to copy: {0} and {1} are the same".format(in_path, out_path))
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to transfer file to {0}".format(out_path))
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from local to local -- for copatibility '''
|
||||
#vvv("FETCH {0} TO {1}".format(in_path, out_path), host=self.host)
|
||||
self._display.vvv("{0} FETCH {1} TO {2}".format(self._connection_info.remote_addr, in_path, out_path))
|
||||
self.put_file(in_path, out_path)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
self._connected = False
|
||||
390
lib/ansible/plugins/connections/paramiko_ssh.py
Normal file
390
lib/ansible/plugins/connections/paramiko_ssh.py
Normal file
@@ -0,0 +1,390 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# ---
|
||||
# The paramiko transport is provided because many distributions, in particular EL6 and before
|
||||
# do not support ControlPersist in their SSH implementations. This is needed on the Ansible
|
||||
# control machine to be reasonably efficient with connections. Thus paramiko is faster
|
||||
# for most users on these platforms. Users with ControlPersist capability can consider
|
||||
# using -c ssh or configuring the transport in ansible.cfg.
|
||||
|
||||
import warnings
|
||||
import os
|
||||
import pipes
|
||||
import socket
|
||||
import random
|
||||
import logging
|
||||
import tempfile
|
||||
import traceback
|
||||
import fcntl
|
||||
import re
|
||||
import sys
|
||||
|
||||
from termios import tcflush, TCIFLUSH
|
||||
from binascii import hexlify
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
|
||||
AUTHENTICITY_MSG="""
|
||||
paramiko: The authenticity of host '%s' can't be established.
|
||||
The %s key fingerprint is %s.
|
||||
Are you sure you want to continue connecting (yes/no)?
|
||||
"""
|
||||
|
||||
# prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/
|
||||
HAVE_PARAMIKO=False
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
try:
|
||||
import paramiko
|
||||
HAVE_PARAMIKO=True
|
||||
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
class MyAddPolicy(object):
|
||||
"""
|
||||
Based on AutoAddPolicy in paramiko so we can determine when keys are added
|
||||
and also prompt for input.
|
||||
|
||||
Policy for automatically adding the hostname and new host key to the
|
||||
local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
|
||||
"""
|
||||
|
||||
def __init__(self, new_stdin):
|
||||
self._new_stdin = new_stdin
|
||||
|
||||
def missing_host_key(self, client, hostname, key):
|
||||
|
||||
if C.HOST_KEY_CHECKING:
|
||||
|
||||
# FIXME: need to fix lock file stuff
|
||||
#fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
||||
#fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
||||
|
||||
old_stdin = sys.stdin
|
||||
sys.stdin = self._new_stdin
|
||||
|
||||
# clear out any premature input on sys.stdin
|
||||
tcflush(sys.stdin, TCIFLUSH)
|
||||
|
||||
fingerprint = hexlify(key.get_fingerprint())
|
||||
ktype = key.get_name()
|
||||
|
||||
inp = raw_input(AUTHENTICITY_MSG % (hostname, ktype, fingerprint))
|
||||
sys.stdin = old_stdin
|
||||
|
||||
if inp not in ['yes','y','']:
|
||||
# FIXME: lock file stuff
|
||||
#fcntl.flock(self.runner.output_lockfile, fcntl.LOCK_UN)
|
||||
#fcntl.flock(self.runner.process_lockfile, fcntl.LOCK_UN)
|
||||
raise AnsibleError("host connection rejected by user")
|
||||
|
||||
# FIXME: lock file stuff
|
||||
#fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
|
||||
#fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
key._added_by_ansible_this_time = True
|
||||
|
||||
# existing implementation below:
|
||||
client._host_keys.add(hostname, key.get_name(), key)
|
||||
|
||||
# host keys are actually saved in close() function below
|
||||
# in order to control ordering.
|
||||
|
||||
|
||||
# keep connection objects on a per host basis to avoid repeated attempts to reconnect
|
||||
|
||||
SSH_CONNECTION_CACHE = {}
|
||||
SFTP_CONNECTION_CACHE = {}
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' SSH based connections with Paramiko '''
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object from other classes '''
|
||||
return 'paramiko'
|
||||
|
||||
def _cache_key(self):
|
||||
return "%s__%s__" % (self._connection_info.remote_addr, self._connection_info.remote_user)
|
||||
|
||||
def _connect(self):
|
||||
cache_key = self._cache_key()
|
||||
if cache_key in SSH_CONNECTION_CACHE:
|
||||
self.ssh = SSH_CONNECTION_CACHE[cache_key]
|
||||
else:
|
||||
self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached()
|
||||
return self
|
||||
|
||||
def _connect_uncached(self):
|
||||
''' activates the connection object '''
|
||||
|
||||
if not HAVE_PARAMIKO:
|
||||
raise AnsibleError("paramiko is not installed")
|
||||
|
||||
port = self._connection_info.port or 22
|
||||
self._display.vvv("ESTABLISH CONNECTION FOR USER: %s on PORT %s TO %s" % (self._connection_info.remote_user, port, self._connection_info.remote_addr), host=self._connection_info.remote_addr)
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
|
||||
self.keyfile = os.path.expanduser("~/.ssh/known_hosts")
|
||||
|
||||
if C.HOST_KEY_CHECKING:
|
||||
ssh.load_system_host_keys()
|
||||
|
||||
ssh.set_missing_host_key_policy(MyAddPolicy(self._new_stdin))
|
||||
|
||||
allow_agent = True
|
||||
|
||||
if self._connection_info.password is not None:
|
||||
allow_agent = False
|
||||
|
||||
try:
|
||||
key_filename = None
|
||||
if self._connection_info.private_key_file:
|
||||
key_filename = os.path.expanduser(self._connection_info.private_key_file)
|
||||
|
||||
ssh.connect(
|
||||
self._connection_info.remote_addr,
|
||||
username=self._connection_info.remote_user,
|
||||
allow_agent=allow_agent,
|
||||
look_for_keys=True,
|
||||
key_filename=key_filename,
|
||||
password=self._connection_info.password,
|
||||
timeout=self._connection_info.timeout,
|
||||
port=port,
|
||||
)
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if "PID check failed" in msg:
|
||||
raise AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible")
|
||||
elif "Private key file is encrypted" in msg:
|
||||
msg = 'ssh %s@%s:%s : %s\nTo connect as a different user, use -u <username>.' % (
|
||||
self._connection_info.remote_user, self._connection_info.remote_addr, port, msg)
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
else:
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
|
||||
return ssh
|
||||
|
||||
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
bufsize = 4096
|
||||
|
||||
try:
|
||||
self.ssh.get_transport().set_keepalive(5)
|
||||
chan = self.ssh.get_transport().open_session()
|
||||
except Exception as e:
|
||||
msg = "Failed to open session"
|
||||
if len(str(e)) > 0:
|
||||
msg += ": %s" % str(e)
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
|
||||
# sudo usually requires a PTY (cf. requiretty option), therefore
|
||||
# we give it one by default (pty=True in ansble.cfg), and we try
|
||||
# to initialise from the calling environment
|
||||
if C.PARAMIKO_PTY:
|
||||
chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0)))
|
||||
|
||||
self._display.vvv("EXEC %s" % cmd, host=self._connection_info.remote_addr)
|
||||
|
||||
no_prompt_out = ''
|
||||
no_prompt_err = ''
|
||||
become_output = ''
|
||||
|
||||
try:
|
||||
chan.exec_command(cmd)
|
||||
if self._connection_info.become_pass:
|
||||
while True:
|
||||
if success_key in become_output or \
|
||||
(prompt and become_output.endswith(prompt)) or \
|
||||
utils.su_prompts.check_su_prompt(become_output):
|
||||
break
|
||||
chunk = chan.recv(bufsize)
|
||||
if not chunk:
|
||||
if 'unknown user' in become_output:
|
||||
raise AnsibleError(
|
||||
'user %s does not exist' % become_user)
|
||||
else:
|
||||
raise AnsibleError('ssh connection ' +
|
||||
'closed waiting for password prompt')
|
||||
become_output += chunk
|
||||
if success_key not in become_output:
|
||||
if self._connection_info.become:
|
||||
chan.sendall(self._connection_info.become_pass + '\n')
|
||||
else:
|
||||
no_prompt_out += become_output
|
||||
no_prompt_err += become_output
|
||||
except socket.timeout:
|
||||
raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + become_output)
|
||||
|
||||
stdout = ''.join(chan.makefile('rb', bufsize))
|
||||
stderr = ''.join(chan.makefile_stderr('rb', bufsize))
|
||||
|
||||
return (chan.recv_exit_status(), '', no_prompt_out + stdout, no_prompt_out + stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
|
||||
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
|
||||
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
|
||||
try:
|
||||
self.sftp = self.ssh.open_sftp()
|
||||
except Exception as e:
|
||||
raise AnsibleError("failed to open a SFTP connection (%s)" % e)
|
||||
|
||||
try:
|
||||
self.sftp.put(in_path, out_path)
|
||||
except IOError:
|
||||
raise AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def _connect_sftp(self):
|
||||
|
||||
cache_key = "%s__%s__" % (self._connection_info.remote_addr, self._connection_info.remote_user)
|
||||
if cache_key in SFTP_CONNECTION_CACHE:
|
||||
return SFTP_CONNECTION_CACHE[cache_key]
|
||||
else:
|
||||
result = SFTP_CONNECTION_CACHE[cache_key] = self.connect().ssh.open_sftp()
|
||||
return result
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' save a remote file to the specified path '''
|
||||
|
||||
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
|
||||
|
||||
try:
|
||||
self.sftp = self._connect_sftp()
|
||||
except Exception as e:
|
||||
raise AnsibleError("failed to open a SFTP connection (%s)", e)
|
||||
|
||||
try:
|
||||
self.sftp.get(in_path, out_path)
|
||||
except IOError:
|
||||
raise AnsibleError("failed to transfer file from %s" % in_path)
|
||||
|
||||
def _any_keys_added(self):
|
||||
|
||||
added_any = False
|
||||
for hostname, keys in self.ssh._host_keys.iteritems():
|
||||
for keytype, key in keys.iteritems():
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if added_this_time:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _save_ssh_host_keys(self, filename):
|
||||
'''
|
||||
not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
|
||||
don't complain about it :)
|
||||
'''
|
||||
|
||||
if not self._any_keys_added():
|
||||
return False
|
||||
|
||||
path = os.path.expanduser("~/.ssh")
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
f = open(filename, 'w')
|
||||
|
||||
for hostname, keys in self.ssh._host_keys.iteritems():
|
||||
|
||||
for keytype, key in keys.iteritems():
|
||||
|
||||
# was f.write
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if not added_this_time:
|
||||
f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
|
||||
|
||||
for hostname, keys in self.ssh._host_keys.iteritems():
|
||||
|
||||
for keytype, key in keys.iteritems():
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if added_this_time:
|
||||
f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
|
||||
|
||||
f.close()
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection '''
|
||||
|
||||
cache_key = self._cache_key()
|
||||
SSH_CONNECTION_CACHE.pop(cache_key, None)
|
||||
SFTP_CONNECTION_CACHE.pop(cache_key, None)
|
||||
|
||||
if self.sftp is not None:
|
||||
self.sftp.close()
|
||||
|
||||
if C.HOST_KEY_CHECKING and C.PARAMIKO_RECORD_HOST_KEYS and self._any_keys_added():
|
||||
|
||||
# add any new SSH host keys -- warning -- this could be slow
|
||||
lockfile = self.keyfile.replace("known_hosts",".known_hosts.lock")
|
||||
dirname = os.path.dirname(self.keyfile)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
KEY_LOCK = open(lockfile, 'w')
|
||||
fcntl.lockf(KEY_LOCK, fcntl.LOCK_EX)
|
||||
|
||||
try:
|
||||
# just in case any were added recently
|
||||
|
||||
self.ssh.load_system_host_keys()
|
||||
self.ssh._host_keys.update(self.ssh._system_host_keys)
|
||||
|
||||
# gather information about the current key file, so
|
||||
# we can ensure the new file has the correct mode/owner
|
||||
|
||||
key_dir = os.path.dirname(self.keyfile)
|
||||
key_stat = os.stat(self.keyfile)
|
||||
|
||||
# Save the new keys to a temporary file and move it into place
|
||||
# rather than rewriting the file. We set delete=False because
|
||||
# the file will be moved into place rather than cleaned up.
|
||||
|
||||
tmp_keyfile = tempfile.NamedTemporaryFile(dir=key_dir, delete=False)
|
||||
os.chmod(tmp_keyfile.name, key_stat.st_mode & 07777)
|
||||
os.chown(tmp_keyfile.name, key_stat.st_uid, key_stat.st_gid)
|
||||
|
||||
self._save_ssh_host_keys(tmp_keyfile.name)
|
||||
tmp_keyfile.close()
|
||||
|
||||
os.rename(tmp_keyfile.name, self.keyfile)
|
||||
|
||||
except:
|
||||
|
||||
# unable to save keys, including scenario when key was invalid
|
||||
# and caught earlier
|
||||
traceback.print_exc()
|
||||
pass
|
||||
fcntl.lockf(KEY_LOCK, fcntl.LOCK_UN)
|
||||
|
||||
self.ssh.close()
|
||||
|
||||
462
lib/ansible/plugins/connections/ssh.py
Normal file
462
lib/ansible/plugins/connections/ssh.py
Normal file
@@ -0,0 +1,462 @@
|
||||
# (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/>.
|
||||
#
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import shlex
|
||||
import pipes
|
||||
import random
|
||||
import select
|
||||
import fcntl
|
||||
import hmac
|
||||
import pwd
|
||||
import gettext
|
||||
import pty
|
||||
from hashlib import sha1
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' ssh based connections '''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# SSH connection specific init stuff
|
||||
self.HASHED_KEY_MAGIC = "|1|"
|
||||
self._has_pipelining = True
|
||||
|
||||
# FIXME: move the lockfile locations to ActionBase?
|
||||
#fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
||||
#self.cp_dir = utils.prepare_writeable_dir('$HOME/.ansible/cp',mode=0700)
|
||||
self._cp_dir = '/tmp'
|
||||
#fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
||||
|
||||
super(Connection, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object from other classes '''
|
||||
return 'ssh'
|
||||
|
||||
def _connect(self):
|
||||
''' connect to the remote host '''
|
||||
|
||||
self._display.vvv("ESTABLISH SSH CONNECTION FOR USER: {0}".format(self._connection_info.remote_user), host=self._connection_info.remote_addr)
|
||||
|
||||
if self._connected:
|
||||
return self
|
||||
|
||||
self._common_args = []
|
||||
extra_args = C.ANSIBLE_SSH_ARGS
|
||||
if extra_args is not None:
|
||||
# make sure there is no empty string added as this can produce weird errors
|
||||
self._common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()]
|
||||
else:
|
||||
self._common_args += (
|
||||
"-o", "ControlMaster=auto",
|
||||
"-o", "ControlPersist=60s",
|
||||
"-o", "ControlPath=\"{0}\"".format(C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir)),
|
||||
)
|
||||
|
||||
cp_in_use = False
|
||||
cp_path_set = False
|
||||
for arg in self._common_args:
|
||||
if "ControlPersist" in arg:
|
||||
cp_in_use = True
|
||||
if "ControlPath" in arg:
|
||||
cp_path_set = True
|
||||
|
||||
if cp_in_use and not cp_path_set:
|
||||
self._common_args += ("-o", "ControlPath=\"{0}\"".format(
|
||||
C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir))
|
||||
)
|
||||
|
||||
if not C.HOST_KEY_CHECKING:
|
||||
self._common_args += ("-o", "StrictHostKeyChecking=no")
|
||||
|
||||
if self._connection_info.port is not None:
|
||||
self._common_args += ("-o", "Port={0}".format(self._connection_info.port))
|
||||
# FIXME: need to get this from connection info
|
||||
#if self.private_key_file is not None:
|
||||
# self._common_args += ("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(self.private_key_file)))
|
||||
#elif self.runner.private_key_file is not None:
|
||||
# self._common_args += ("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(self.runner.private_key_file)))
|
||||
if self._connection_info.password:
|
||||
self._common_args += ("-o", "GSSAPIAuthentication=no",
|
||||
"-o", "PubkeyAuthentication=no")
|
||||
else:
|
||||
self._common_args += ("-o", "KbdInteractiveAuthentication=no",
|
||||
"-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
|
||||
"-o", "PasswordAuthentication=no")
|
||||
if self._connection_info.remote_user is not None and self._connection_info.remote_user != pwd.getpwuid(os.geteuid())[0]:
|
||||
self._common_args += ("-o", "User={0}".format(self._connection_info.remote_user))
|
||||
# FIXME: figure out where this goes
|
||||
#self._common_args += ("-o", "ConnectTimeout={0}".format(self.runner.timeout))
|
||||
self._common_args += ("-o", "ConnectTimeout=15")
|
||||
|
||||
self._connected = True
|
||||
|
||||
return self
|
||||
|
||||
def _run(self, cmd, indata):
|
||||
if indata:
|
||||
# do not use pseudo-pty
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin = p.stdin
|
||||
else:
|
||||
# try to use upseudo-pty
|
||||
try:
|
||||
# Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
|
||||
master, slave = pty.openpty()
|
||||
p = subprocess.Popen(cmd, stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin = os.fdopen(master, 'w', 0)
|
||||
os.close(slave)
|
||||
except:
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin = p.stdin
|
||||
|
||||
return (p, stdin)
|
||||
|
||||
def _password_cmd(self):
|
||||
if self._connection_info.password:
|
||||
try:
|
||||
p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
except OSError:
|
||||
raise AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
|
||||
(self.rfd, self.wfd) = os.pipe()
|
||||
return ("sshpass", "-d{0}".format(self.rfd))
|
||||
return []
|
||||
|
||||
def _send_password(self):
|
||||
if self._connection_info.password:
|
||||
os.close(self.rfd)
|
||||
os.write(self.wfd, "{0}\n".format(self._connection_info.password))
|
||||
os.close(self.wfd)
|
||||
|
||||
def _communicate(self, p, stdin, indata, su=False, sudoable=False, prompt=None):
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
||||
stdout = ''
|
||||
stderr = ''
|
||||
rpipes = [p.stdout, p.stderr]
|
||||
if indata:
|
||||
try:
|
||||
stdin.write(indata)
|
||||
stdin.close()
|
||||
except:
|
||||
raise AnsibleConnectionFailure('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
|
||||
# Read stdout/stderr from process
|
||||
while True:
|
||||
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
||||
|
||||
# FIXME: su/sudo stuff
|
||||
# fail early if the sudo/su password is wrong
|
||||
#if self.runner.sudo and sudoable:
|
||||
# if self.runner.sudo_pass:
|
||||
# incorrect_password = gettext.dgettext(
|
||||
# "sudo", "Sorry, try again.")
|
||||
# if stdout.endswith("%s\r\n%s" % (incorrect_password,
|
||||
# prompt)):
|
||||
# raise AnsibleError('Incorrect sudo password')
|
||||
#
|
||||
# if stdout.endswith(prompt):
|
||||
# raise AnsibleError('Missing sudo password')
|
||||
#
|
||||
#if self.runner.su and su and self.runner.su_pass:
|
||||
# incorrect_password = gettext.dgettext(
|
||||
# "su", "Sorry")
|
||||
# if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
||||
# raise AnsibleError('Incorrect su password')
|
||||
|
||||
if p.stdout in rfd:
|
||||
dat = os.read(p.stdout.fileno(), 9000)
|
||||
stdout += dat
|
||||
if dat == '':
|
||||
rpipes.remove(p.stdout)
|
||||
if p.stderr in rfd:
|
||||
dat = os.read(p.stderr.fileno(), 9000)
|
||||
stderr += dat
|
||||
if dat == '':
|
||||
rpipes.remove(p.stderr)
|
||||
# only break out if no pipes are left to read or
|
||||
# the pipes are completely read and
|
||||
# the process is terminated
|
||||
if (not rpipes or not rfd) and p.poll() is not None:
|
||||
break
|
||||
# No pipes are left to read but process is not yet terminated
|
||||
# Only then it is safe to wait for the process to be finished
|
||||
# NOTE: Actually p.poll() is always None here if rpipes is empty
|
||||
elif not rpipes and p.poll() == None:
|
||||
p.wait()
|
||||
# The process is terminated. Since no pipes to read from are
|
||||
# left, there is no need to call select() again.
|
||||
break
|
||||
# close stdin after process is terminated and stdout/stderr are read
|
||||
# completely (see also issue #848)
|
||||
stdin.close()
|
||||
return (p.returncode, stdout, stderr)
|
||||
|
||||
def not_in_host_file(self, host):
|
||||
if 'USER' in os.environ:
|
||||
user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
|
||||
else:
|
||||
user_host_file = "~/.ssh/known_hosts"
|
||||
user_host_file = os.path.expanduser(user_host_file)
|
||||
|
||||
host_file_list = []
|
||||
host_file_list.append(user_host_file)
|
||||
host_file_list.append("/etc/ssh/ssh_known_hosts")
|
||||
host_file_list.append("/etc/ssh/ssh_known_hosts2")
|
||||
|
||||
hfiles_not_found = 0
|
||||
for hf in host_file_list:
|
||||
if not os.path.exists(hf):
|
||||
hfiles_not_found += 1
|
||||
continue
|
||||
try:
|
||||
host_fh = open(hf)
|
||||
except IOError as e:
|
||||
hfiles_not_found += 1
|
||||
continue
|
||||
else:
|
||||
data = host_fh.read()
|
||||
host_fh.close()
|
||||
|
||||
for line in data.split("\n"):
|
||||
if line is None or " " not in line:
|
||||
continue
|
||||
tokens = line.split()
|
||||
if not tokens:
|
||||
continue
|
||||
if tokens[0].find(self.HASHED_KEY_MAGIC) == 0:
|
||||
# this is a hashed known host entry
|
||||
try:
|
||||
(kn_salt,kn_host) = tokens[0][len(self.HASHED_KEY_MAGIC):].split("|",2)
|
||||
hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
|
||||
hash.update(host)
|
||||
if hash.digest() == kn_host.decode('base64'):
|
||||
return False
|
||||
except:
|
||||
# invalid hashed host key, skip it
|
||||
continue
|
||||
else:
|
||||
# standard host file entry
|
||||
if host in tokens[0]:
|
||||
return False
|
||||
|
||||
if (hfiles_not_found == len(host_file_list)):
|
||||
self._display.vvv("EXEC previous known host file not found for {0}".format(host))
|
||||
return True
|
||||
|
||||
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
ssh_cmd = self._password_cmd()
|
||||
ssh_cmd += ("ssh", "-C")
|
||||
if not in_data:
|
||||
# we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
|
||||
# inside a tty automatically invokes the python interactive-mode but the modules are not
|
||||
# compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
|
||||
ssh_cmd.append("-tt")
|
||||
if self._connection_info.verbosity > 3:
|
||||
ssh_cmd.append("-vvv")
|
||||
else:
|
||||
ssh_cmd.append("-q")
|
||||
ssh_cmd += self._common_args
|
||||
|
||||
# FIXME: ipv6 stuff needs to be figured out. It's in the connection info, however
|
||||
# not sure if it's all working yet so this remains commented out
|
||||
#if self._ipv6:
|
||||
# ssh_cmd += ['-6']
|
||||
ssh_cmd.append(self._connection_info.remote_addr)
|
||||
|
||||
ssh_cmd.append(cmd)
|
||||
self._display.vvv("EXEC {0}".format(' '.join(ssh_cmd)), host=self._connection_info.remote_addr)
|
||||
|
||||
not_in_host_file = self.not_in_host_file(self._connection_info.remote_addr)
|
||||
|
||||
# FIXME: move the locations of these lock files, same as init above
|
||||
#if C.HOST_KEY_CHECKING and not_in_host_file:
|
||||
# # lock around the initial SSH connectivity so the user prompt about whether to add
|
||||
# # the host to known hosts is not intermingled with multiprocess output.
|
||||
# fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
||||
# fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
||||
|
||||
# create process
|
||||
(p, stdin) = self._run(ssh_cmd, in_data)
|
||||
|
||||
self._send_password()
|
||||
|
||||
no_prompt_out = ''
|
||||
no_prompt_err = ''
|
||||
# FIXME: su/sudo stuff
|
||||
#if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
|
||||
# (self.runner.su and su and self.runner.su_pass):
|
||||
# # several cases are handled for sudo privileges with password
|
||||
# # * NOPASSWD (tty & no-tty): detect success_key on stdout
|
||||
# # * without NOPASSWD:
|
||||
# # * detect prompt on stdout (tty)
|
||||
# # * detect prompt on stderr (no-tty)
|
||||
# fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
||||
# fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
# fcntl.fcntl(p.stderr, fcntl.F_SETFL,
|
||||
# fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
# sudo_output = ''
|
||||
# sudo_errput = ''
|
||||
#
|
||||
# while True:
|
||||
# if success_key in sudo_output or \
|
||||
# (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
|
||||
# (self.runner.su_pass and utils.su_prompts.check_su_prompt(sudo_output)):
|
||||
# break
|
||||
#
|
||||
# rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
|
||||
# [p.stdout], self.runner.timeout)
|
||||
# if p.stderr in rfd:
|
||||
# chunk = p.stderr.read()
|
||||
# if not chunk:
|
||||
# raise AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
||||
# sudo_errput += chunk
|
||||
# incorrect_password = gettext.dgettext(
|
||||
# "sudo", "Sorry, try again.")
|
||||
# if sudo_errput.strip().endswith("%s%s" % (prompt, incorrect_password)):
|
||||
# raise AnsibleError('Incorrect sudo password')
|
||||
# elif sudo_errput.endswith(prompt):
|
||||
# stdin.write(self.runner.sudo_pass + '\n')
|
||||
#
|
||||
# if p.stdout in rfd:
|
||||
# chunk = p.stdout.read()
|
||||
# if not chunk:
|
||||
# raise AnsibleError('ssh connection closed waiting for sudo or su password prompt')
|
||||
# sudo_output += chunk
|
||||
#
|
||||
# if not rfd:
|
||||
# # timeout. wrap up process communication
|
||||
# stdout = p.communicate()
|
||||
# raise AnsibleError('ssh connection error waiting for sudo or su password prompt')
|
||||
#
|
||||
# if success_key not in sudo_output:
|
||||
# if sudoable:
|
||||
# stdin.write(self.runner.sudo_pass + '\n')
|
||||
# elif su:
|
||||
# stdin.write(self.runner.su_pass + '\n')
|
||||
# else:
|
||||
# no_prompt_out += sudo_output
|
||||
# no_prompt_err += sudo_errput
|
||||
|
||||
#(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, su=su, sudoable=sudoable, prompt=prompt)
|
||||
# FIXME: the prompt won't be here anymore
|
||||
prompt=""
|
||||
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, prompt=prompt)
|
||||
|
||||
#if C.HOST_KEY_CHECKING and not_in_host_file:
|
||||
# # lock around the initial SSH connectivity so the user prompt about whether to add
|
||||
# # the host to known hosts is not intermingled with multiprocess output.
|
||||
# fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
|
||||
# fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
||||
controlpersisterror = 'Bad configuration option: ControlPersist' in stderr or 'unknown configuration option: ControlPersist' in stderr
|
||||
|
||||
if C.HOST_KEY_CHECKING:
|
||||
if ssh_cmd[0] == "sshpass" and p.returncode == 6:
|
||||
raise AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host\'s fingerprint to your known_hosts file to manage this host.')
|
||||
|
||||
if p.returncode != 0 and controlpersisterror:
|
||||
raise AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" (or ssh_args in [ssh_connection] section of the config file) before running again')
|
||||
# FIXME: module name isn't in runner
|
||||
#if p.returncode == 255 and (in_data or self.runner.module_name == 'raw'):
|
||||
if p.returncode == 255 and in_data:
|
||||
raise AnsibleConnectionFailure('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
|
||||
|
||||
return (p.returncode, '', no_prompt_out + stdout, no_prompt_err + stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
self._display.vvv("PUT {0} TO {1}".format(in_path, out_path), host=self._connection_info.remote_addr)
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: {0}".format(in_path))
|
||||
cmd = self._password_cmd()
|
||||
|
||||
# FIXME: make a function, used in all 3 methods EXEC/PUT/FETCH
|
||||
host = self._connection_info.remote_addr
|
||||
|
||||
# FIXME: ipv6 stuff needs to be figured out. It's in the connection info, however
|
||||
# not sure if it's all working yet so this remains commented out
|
||||
#if self._ipv6:
|
||||
# host = '[%s]' % host
|
||||
|
||||
if C.DEFAULT_SCP_IF_SSH:
|
||||
cmd.append('scp')
|
||||
cmd += self._common_args
|
||||
cmd.append(in_path,host + ":" + pipes.quote(out_path))
|
||||
indata = None
|
||||
else:
|
||||
cmd.append('sftp')
|
||||
cmd += self._common_args
|
||||
cmd.append(host)
|
||||
indata = "put {0} {1}\n".format(pipes.quote(in_path), pipes.quote(out_path))
|
||||
|
||||
(p, stdin) = self._run(cmd, indata)
|
||||
|
||||
self._send_password()
|
||||
|
||||
(returncode, stdout, stderr) = self._communicate(p, stdin, indata)
|
||||
|
||||
if returncode != 0:
|
||||
raise AnsibleError("failed to transfer file to {0}:\n{1}\n{2}".format(out_path, stdout, stderr))
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from remote to local '''
|
||||
self._display.vvv("FETCH {0} TO {1}".format(in_path, out_path), host=self._connection_info.remote_addr)
|
||||
cmd = self._password_cmd()
|
||||
|
||||
# FIXME: make a function, used in all 3 methods EXEC/PUT/FETCH
|
||||
host = self._connection_info.remote_addr
|
||||
|
||||
# FIXME: ipv6 stuff needs to be figured out. It's in the connection info, however
|
||||
# not sure if it's all working yet so this remains commented out
|
||||
#if self._ipv6:
|
||||
# host = '[%s]' % self._connection_info.remote_addr
|
||||
|
||||
if C.DEFAULT_SCP_IF_SSH:
|
||||
cmd.append('scp')
|
||||
cmd += self._common_args
|
||||
cmd += ('{0}:{1}'.format(host, in_path), out_path)
|
||||
indata = None
|
||||
else:
|
||||
cmd.append('sftp')
|
||||
cmd += self._common_args
|
||||
cmd.append(host)
|
||||
indata = "get {0} {1}\n".format(in_path, out_path)
|
||||
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
self._send_password()
|
||||
stdout, stderr = p.communicate(indata)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError("failed to transfer file from {0}:\n{1}\n{2}".format(in_path, stdout, stderr))
|
||||
|
||||
def close(self):
|
||||
''' not applicable since we're executing openssh binaries '''
|
||||
self._connected = False
|
||||
|
||||
277
lib/ansible/plugins/connections/winrm.py
Normal file
277
lib/ansible/plugins/connections/winrm.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import traceback
|
||||
|
||||
from six.moves.urllib import parse
|
||||
|
||||
try:
|
||||
from winrm import Response
|
||||
from winrm.exceptions import WinRMTransportError
|
||||
from winrm.protocol import Protocol
|
||||
except ImportError:
|
||||
raise AnsibleError("winrm is not installed")
|
||||
|
||||
HAVE_KERBEROS = False
|
||||
try:
|
||||
import kerberos
|
||||
HAVE_KERBEROS = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
from ansible.plugins import shell_loader
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
'''WinRM connections over HTTP/HTTPS.'''
|
||||
|
||||
transport_schemes = {
|
||||
'http': [('kerberos', 'http'), ('plaintext', 'http'), ('plaintext', 'https')],
|
||||
'https': [('kerberos', 'https'), ('plaintext', 'https')],
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.has_pipelining = False
|
||||
self.default_suffixes = ['.ps1', '']
|
||||
self.protocol = None
|
||||
self.shell_id = None
|
||||
self.delegate = None
|
||||
|
||||
self._shell = shell_loader.get('powershell')
|
||||
|
||||
# TODO: Add runas support
|
||||
self.become_methods_supported=[]
|
||||
|
||||
super(Connection, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object from other classes '''
|
||||
return 'winrm'
|
||||
|
||||
def _winrm_connect(self):
|
||||
'''
|
||||
Establish a WinRM connection over HTTP/HTTPS.
|
||||
'''
|
||||
port = self._connection_info.port or 5986
|
||||
self._display.vvv("ESTABLISH WINRM CONNECTION FOR USER: %s on PORT %s TO %s" % \
|
||||
(self._connection_info.remote_user, port, self._connection_info.remote_addr), host=self._connection_info.remote_addr)
|
||||
netloc = '%s:%d' % (self._connection_info.remote_addr, port)
|
||||
exc = None
|
||||
for transport, scheme in self.transport_schemes['http' if port == 5985 else 'https']:
|
||||
if transport == 'kerberos' and (not HAVE_KERBEROS or not '@' in self._connection_info.remote_user):
|
||||
continue
|
||||
|
||||
if transport == 'kerberos':
|
||||
realm = self._connection_info.remote_user.split('@', 1)[1].strip() or None
|
||||
else:
|
||||
realm = None
|
||||
|
||||
endpoint = parse.urlunsplit((scheme, netloc, '/wsman', '', ''))
|
||||
|
||||
self._display.vvvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint), host=self._connection_info.remote_addr)
|
||||
protocol = Protocol(
|
||||
endpoint,
|
||||
transport=transport,
|
||||
username=self._connection_info.remote_user,
|
||||
password=self._connection_info.password,
|
||||
realm=realm
|
||||
)
|
||||
|
||||
try:
|
||||
protocol.send_message('')
|
||||
return protocol
|
||||
except WinRMTransportError as exc:
|
||||
err_msg = str(exc)
|
||||
if re.search(r'Operation\s+?timed\s+?out', err_msg, re.I):
|
||||
raise AnsibleError("the connection attempt timed out")
|
||||
m = re.search(r'Code\s+?(\d{3})', err_msg)
|
||||
if m:
|
||||
code = int(m.groups()[0])
|
||||
if code == 401:
|
||||
raise AnsibleError("the username/password specified for this server was incorrect")
|
||||
elif code == 411:
|
||||
return protocol
|
||||
self._display.vvvvv('WINRM CONNECTION ERROR: %s' % err_msg, host=self._connection_info.remote_addr)
|
||||
continue
|
||||
if exc:
|
||||
raise AnsibleError(str(exc))
|
||||
|
||||
def _winrm_exec(self, command, args=(), from_exec=False):
|
||||
if from_exec:
|
||||
self._display.vvvvv("WINRM EXEC %r %r" % (command, args), host=self._connection_info.remote_addr)
|
||||
else:
|
||||
self._display.vvvvvv("WINRM EXEC %r %r" % (command, args), host=self._connection_info.remote_addr)
|
||||
if not self.protocol:
|
||||
self.protocol = self._winrm_connect()
|
||||
if not self.shell_id:
|
||||
self.shell_id = self.protocol.open_shell()
|
||||
command_id = None
|
||||
try:
|
||||
command_id = self.protocol.run_command(self.shell_id, command, args)
|
||||
response = Response(self.protocol.get_command_output(self.shell_id, command_id))
|
||||
if from_exec:
|
||||
self._display.vvvvv('WINRM RESULT %r' % response, host=self._connection_info.remote_addr)
|
||||
else:
|
||||
self._display.vvvvvv('WINRM RESULT %r' % response, host=self._connection_info.remote_addr)
|
||||
self._display.vvvvvv('WINRM STDOUT %s' % response.std_out, host=self._connection_info.remote_addr)
|
||||
self._display.vvvvvv('WINRM STDERR %s' % response.std_err, host=self._connection_info.remote_addr)
|
||||
return response
|
||||
finally:
|
||||
if command_id:
|
||||
self.protocol.cleanup_command(self.shell_id, command_id)
|
||||
|
||||
def _connect(self):
|
||||
if not self.protocol:
|
||||
self.protocol = self._winrm_connect()
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
|
||||
|
||||
cmd = cmd.encode('utf-8')
|
||||
cmd_parts = shlex.split(cmd, posix=False)
|
||||
if '-EncodedCommand' in cmd_parts:
|
||||
encoded_cmd = cmd_parts[cmd_parts.index('-EncodedCommand') + 1]
|
||||
decoded_cmd = base64.b64decode(encoded_cmd)
|
||||
self._display.vvv("EXEC %s" % decoded_cmd, host=self._connection_info.remote_addr)
|
||||
else:
|
||||
self._display.vvv("EXEC %s" % cmd, host=self._connection_info.remote_addr)
|
||||
# For script/raw support.
|
||||
if cmd_parts and cmd_parts[0].lower().endswith('.ps1'):
|
||||
script = self._shell._build_file_cmd(cmd_parts, quote_args=False)
|
||||
cmd_parts = self._shell._encode_script(script, as_list=True)
|
||||
try:
|
||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to exec cmd %s" % cmd)
|
||||
return (result.status_code, '', result.std_out.encode('utf-8'), result.std_err.encode('utf-8'))
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
with open(in_path) as in_file:
|
||||
in_size = os.path.getsize(in_path)
|
||||
script_template = '''
|
||||
$s = [System.IO.File]::OpenWrite("%s");
|
||||
[void]$s.Seek(%d, [System.IO.SeekOrigin]::Begin);
|
||||
$b = [System.Convert]::FromBase64String("%s");
|
||||
[void]$s.Write($b, 0, $b.length);
|
||||
[void]$s.SetLength(%d);
|
||||
[void]$s.Close();
|
||||
'''
|
||||
# Determine max size of data we can pass per command.
|
||||
script = script_template % (self._shell._escape(out_path), in_size, '', in_size)
|
||||
cmd = self._shell._encode_script(script)
|
||||
# Encode script with no data, subtract its length from 8190 (max
|
||||
# windows command length), divide by 2.67 (UTF16LE base64 command
|
||||
# encoding), then by 1.35 again (data base64 encoding).
|
||||
buffer_size = int(((8190 - len(cmd)) / 2.67) / 1.35)
|
||||
for offset in xrange(0, in_size, buffer_size):
|
||||
try:
|
||||
out_data = in_file.read(buffer_size)
|
||||
if offset == 0:
|
||||
if out_data.lower().startswith('#!powershell') and not out_path.lower().endswith('.ps1'):
|
||||
out_path = out_path + '.ps1'
|
||||
b64_data = base64.b64encode(out_data)
|
||||
script = script_template % (self._shell._escape(out_path), offset, b64_data, in_size)
|
||||
self._display.vvvvv("WINRM PUT %s to %s (offset=%d size=%d)" % (in_path, out_path, offset, len(out_data)), host=self._connection_info.remote_addr)
|
||||
cmd_parts = self._shell._encode_script(script, as_list=True)
|
||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
||||
if result.status_code != 0:
|
||||
raise IOError(result.std_err.encode('utf-8'))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
out_path = out_path.replace('\\', '/')
|
||||
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
|
||||
buffer_size = 2**19 # 0.5MB chunks
|
||||
if not os.path.exists(os.path.dirname(out_path)):
|
||||
os.makedirs(os.path.dirname(out_path))
|
||||
out_file = None
|
||||
try:
|
||||
offset = 0
|
||||
while True:
|
||||
try:
|
||||
script = '''
|
||||
If (Test-Path -PathType Leaf "%(path)s")
|
||||
{
|
||||
$stream = [System.IO.File]::OpenRead("%(path)s");
|
||||
$stream.Seek(%(offset)d, [System.IO.SeekOrigin]::Begin) | Out-Null;
|
||||
$buffer = New-Object Byte[] %(buffer_size)d;
|
||||
$bytesRead = $stream.Read($buffer, 0, %(buffer_size)d);
|
||||
$bytes = $buffer[0..($bytesRead-1)];
|
||||
[System.Convert]::ToBase64String($bytes);
|
||||
$stream.Close() | Out-Null;
|
||||
}
|
||||
ElseIf (Test-Path -PathType Container "%(path)s")
|
||||
{
|
||||
Write-Host "[DIR]";
|
||||
}
|
||||
Else
|
||||
{
|
||||
Write-Error "%(path)s does not exist";
|
||||
Exit 1;
|
||||
}
|
||||
''' % dict(buffer_size=buffer_size, path=self._shell._escape(in_path), offset=offset)
|
||||
self._display.vvvvv("WINRM FETCH %s to %s (offset=%d)" % (in_path, out_path, offset), host=self._connection_info.remote_addr)
|
||||
cmd_parts = self._shell._encode_script(script, as_list=True)
|
||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
||||
if result.status_code != 0:
|
||||
raise IOError(result.std_err.encode('utf-8'))
|
||||
if result.std_out.strip() == '[DIR]':
|
||||
data = None
|
||||
else:
|
||||
data = base64.b64decode(result.std_out.strip())
|
||||
if data is None:
|
||||
if not os.path.exists(out_path):
|
||||
os.makedirs(out_path)
|
||||
break
|
||||
else:
|
||||
if not out_file:
|
||||
# If out_path is a directory and we're expecting a file, bail out now.
|
||||
if os.path.isdir(out_path):
|
||||
break
|
||||
out_file = open(out_path, 'wb')
|
||||
out_file.write(data)
|
||||
if len(data) < buffer_size:
|
||||
break
|
||||
offset += len(data)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to transfer file to %s" % out_path)
|
||||
finally:
|
||||
if out_file:
|
||||
out_file.close()
|
||||
|
||||
def close(self):
|
||||
if self.protocol and self.shell_id:
|
||||
self.protocol.close_shell(self.shell_id)
|
||||
self.shell_id = None
|
||||
164
lib/ansible/plugins/connections/zone.py
Normal file
164
lib/ansible/plugins/connections/zone.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# and jail.py (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
# (c) 2015, Dagobert Michelsen <dam@baltic-online.de>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import traceback
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from subprocess import Popen,PIPE
|
||||
from ansible import errors
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
class Connection(object):
|
||||
''' Local zone based connections '''
|
||||
|
||||
def _search_executable(self, executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
||||
return cmd
|
||||
|
||||
def list_zones(self):
|
||||
pipe = subprocess.Popen([self.zoneadm_cmd, 'list', '-ip'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
#stdout, stderr = p.communicate()
|
||||
zones = []
|
||||
for l in pipe.stdout.readlines():
|
||||
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
||||
s = l.split(':')
|
||||
if s[1] != 'global':
|
||||
zones.append(s[1])
|
||||
|
||||
return zones
|
||||
|
||||
def get_zone_path(self):
|
||||
#solaris10vm# zoneadm -z cswbuild list -p
|
||||
#-:cswbuild:installed:/zones/cswbuild:479f3c4b-d0c6-e97b-cd04-fd58f2c0238e:native:shared
|
||||
pipe = subprocess.Popen([self.zoneadm_cmd, '-z', self.zone, 'list', '-p'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
#stdout, stderr = p.communicate()
|
||||
path = pipe.stdout.readlines()[0].split(':')[3]
|
||||
return path + '/root'
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.zone = host
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
if os.geteuid() != 0:
|
||||
raise errors.AnsibleError("zone connection requires running as root")
|
||||
|
||||
self.zoneadm_cmd = self._search_executable('zoneadm')
|
||||
self.zlogin_cmd = self._search_executable('zlogin')
|
||||
|
||||
if not self.zone in self.list_zones():
|
||||
raise errors.AnsibleError("incorrect zone name %s" % self.zone)
|
||||
|
||||
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the zone; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL ZONE DIR", host=self.zone)
|
||||
|
||||
return self
|
||||
|
||||
# a modifier
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
local_cmd = [self.zlogin_cmd, self.zone, executable, cmd]
|
||||
else:
|
||||
local_cmd = '%s "%s" %s' % (self.zlogin_cmd, self.zone, cmd)
|
||||
return local_cmd
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable=None, in_data=None):
|
||||
''' run a command on the zone '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We happily ignore privilege escalation
|
||||
if executable == '/bin/sh':
|
||||
executable = None
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.zone)
|
||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def _normalize_path(self, path, prefix):
|
||||
if not path.startswith(os.path.sep):
|
||||
path = os.path.join(os.path.sep, path)
|
||||
normpath = os.path.normpath(path)
|
||||
return os.path.join(prefix, normpath[1:])
|
||||
|
||||
def _copy_file(self, in_path, out_path):
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
try:
|
||||
shutil.copyfile(in_path, out_path)
|
||||
except shutil.Error:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to zone '''
|
||||
|
||||
out_path = self._normalize_path(out_path, self.get_zone_path())
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.zone)
|
||||
|
||||
self._copy_file(in_path, out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from zone to local '''
|
||||
|
||||
in_path = self._normalize_path(in_path, self.get_zone_path())
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.zone)
|
||||
|
||||
self._copy_file(in_path, out_path)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
1
lib/ansible/plugins/filter
Symbolic link
1
lib/ansible/plugins/filter
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../lib/ansible/runner/filter_plugins
|
||||
82
lib/ansible/plugins/inventory/__init__.py
Normal file
82
lib/ansible/plugins/inventory/__init__.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
#############################################
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from six import add_metaclass
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class InventoryParser:
|
||||
'''Abstract Base Class for retrieving inventory information
|
||||
|
||||
Any InventoryParser functions by taking an inven_source. The caller then
|
||||
calls the parser() method. Once parser is called, the caller can access
|
||||
InventoryParser.hosts for a mapping of Host objects and
|
||||
InventoryParser.Groups for a mapping of Group objects.
|
||||
'''
|
||||
|
||||
def __init__(self, inven_source):
|
||||
'''
|
||||
InventoryParser contructors take a source of inventory information
|
||||
that they will parse the host and group information from.
|
||||
'''
|
||||
self.inven_source = inven_source
|
||||
self.reset_parser()
|
||||
|
||||
@abstractmethod
|
||||
def reset_parser(self):
|
||||
'''
|
||||
InventoryParsers generally cache their data once parser() is
|
||||
called. This method initializes any parser state before calling parser
|
||||
again.
|
||||
'''
|
||||
self.hosts = dict()
|
||||
self.groups = dict()
|
||||
self.parsed = False
|
||||
|
||||
def _merge(self, target, addition):
|
||||
'''
|
||||
This method is provided to InventoryParsers to merge host or group
|
||||
dicts since it may take several passes to get all of the data
|
||||
|
||||
Example usage:
|
||||
self.hosts = self.from_ini(filename)
|
||||
new_hosts = self.from_script(scriptname)
|
||||
self._merge(self.hosts, new_hosts)
|
||||
'''
|
||||
for i in addition:
|
||||
if i in target:
|
||||
target[i].merge(addition[i])
|
||||
else:
|
||||
target[i] = addition[i]
|
||||
|
||||
@abstractmethod
|
||||
def parse(self, refresh=False):
|
||||
if refresh:
|
||||
self.reset_parser()
|
||||
if self.parsed:
|
||||
return self.parsed
|
||||
|
||||
# Parse self.inven_sources here
|
||||
pass
|
||||
|
||||
61
lib/ansible/plugins/inventory/aggregate.py
Normal file
61
lib/ansible/plugins/inventory/aggregate.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
#############################################
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from . import InventoryParser
|
||||
#from . ini import InventoryIniParser
|
||||
#from . script import InventoryScriptParser
|
||||
|
||||
class InventoryAggregateParser(InventoryParser):
|
||||
|
||||
def __init__(self, inven_sources):
|
||||
self.inven_source = inven_sources
|
||||
self.hosts = dict()
|
||||
self.groups = dict()
|
||||
|
||||
def reset_parser(self):
|
||||
super(InventoryAggregateParser, self).reset_parser()
|
||||
|
||||
def parse(self, refresh=False):
|
||||
# InventoryDirectoryParser is a InventoryAggregateParser so we avoid
|
||||
# a circular import by importing here
|
||||
from . directory import InventoryAggregateParser
|
||||
if super(InventoryAggregateParser, self).parse(refresh):
|
||||
return self.parsed
|
||||
|
||||
for entry in self.inven_sources:
|
||||
if os.path.sep in entry:
|
||||
# file or directory
|
||||
if os.path.isdir(entry):
|
||||
parser = directory.InventoryDirectoryParser(filename=entry)
|
||||
elif utils.is_executable(entry):
|
||||
parser = InventoryScriptParser(filename=entry)
|
||||
else:
|
||||
parser = InventoryIniParser(filename=entry)
|
||||
else:
|
||||
# hostname
|
||||
parser = HostnameParser(hostname=entry)
|
||||
hosts, groups = parser.parse()
|
||||
self._merge(self.hosts, hosts)
|
||||
self._merge(self.groups, groups)
|
||||
52
lib/ansible/plugins/inventory/directory.py
Normal file
52
lib/ansible/plugins/inventory/directory.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
#############################################
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from . aggregate import InventoryAggregateParser
|
||||
|
||||
class InventoryDirectoryParser(InventoryAggregateParser):
|
||||
|
||||
def __init__(self, inven_directory):
|
||||
directory = inven_directory
|
||||
names = os.listdir(inven_directory)
|
||||
filtered_names = []
|
||||
|
||||
# Clean up the list of filenames
|
||||
for filename in names:
|
||||
# Skip files that end with certain extensions or characters
|
||||
if any(filename.endswith(ext) for ext in ("~", ".orig", ".bak", ".ini", ".retry", ".pyc", ".pyo")):
|
||||
continue
|
||||
# Skip hidden files
|
||||
if filename.startswith('.') and not filename.startswith('.{0}'.format(os.path.sep)):
|
||||
continue
|
||||
# These are things inside of an inventory basedir
|
||||
if filename in ("host_vars", "group_vars", "vars_plugins"):
|
||||
continue
|
||||
fullpath = os.path.join(directory, filename)
|
||||
new_names.append(fullpath)
|
||||
|
||||
super(InventoryDirectoryParser, self).__init__(new_names)
|
||||
|
||||
def parse(self):
|
||||
return super(InventoryDirectoryParser, self).parse()
|
||||
60
lib/ansible/plugins/inventory/ini.py
Normal file
60
lib/ansible/plugins/inventory/ini.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
#############################################
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from . import InventoryParser
|
||||
|
||||
class InventoryIniParser(InventoryAggregateParser):
|
||||
|
||||
def __init__(self, inven_directory):
|
||||
directory = inven_directory
|
||||
names = os.listdir(inven_directory)
|
||||
filtered_names = []
|
||||
|
||||
# Clean up the list of filenames
|
||||
for filename in names:
|
||||
# Skip files that end with certain extensions or characters
|
||||
if any(filename.endswith(ext) for ext in ("~", ".orig", ".bak", ".ini", ".retry", ".pyc", ".pyo")):
|
||||
continue
|
||||
# Skip hidden files
|
||||
if filename.startswith('.') and not filename.startswith('.{0}'.format(os.path.sep)):
|
||||
continue
|
||||
# These are things inside of an inventory basedir
|
||||
if filename in ("host_vars", "group_vars", "vars_plugins"):
|
||||
continue
|
||||
fullpath = os.path.join(directory, filename)
|
||||
new_names.append(fullpath)
|
||||
|
||||
super(InventoryDirectoryParser, self).__init__(new_names)
|
||||
|
||||
def parse(self):
|
||||
return super(InventoryDirectoryParser, self).parse()
|
||||
|
||||
def _before_comment(self, msg):
|
||||
''' what's the part of a string before a comment? '''
|
||||
msg = msg.replace("\#","**NOT_A_COMMENT**")
|
||||
msg = msg.split("#")[0]
|
||||
msg = msg.replace("**NOT_A_COMMENT**","#")
|
||||
return msg
|
||||
|
||||
49
lib/ansible/plugins/lookup/__init__.py
Normal file
49
lib/ansible/plugins/lookup/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
__all__ = ['LookupBase']
|
||||
|
||||
class LookupBase:
|
||||
def __init__(self, loader=None, **kwargs):
|
||||
self._loader = loader
|
||||
|
||||
def _flatten(self, terms):
|
||||
ret = []
|
||||
for term in terms:
|
||||
if isinstance(term, (list, tuple)):
|
||||
ret.extend(term)
|
||||
else:
|
||||
ret.append(term)
|
||||
return ret
|
||||
|
||||
def _combine(self, a, b):
|
||||
results = []
|
||||
for x in a:
|
||||
for y in b:
|
||||
results.append(self._flatten([x,y]))
|
||||
return results
|
||||
|
||||
def _flatten_hash_to_list(self, terms):
|
||||
ret = []
|
||||
for key in terms:
|
||||
ret.append({'key': key, 'value': terms[key]})
|
||||
return ret
|
||||
|
||||
48
lib/ansible/plugins/lookup/cartesian.py
Normal file
48
lib/ansible/plugins/lookup/cartesian.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# (c) 2013, Bradley Young <young.bradley@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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from itertools import product
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
Create the cartesian product of lists
|
||||
[1, 2, 3], [a, b] -> [1, a], [1, b], [2, a], [2, b], [3, a], [3, b]
|
||||
"""
|
||||
|
||||
def __lookup_variabless(self, terms, variables):
|
||||
results = []
|
||||
for x in terms:
|
||||
intermediate = listify_lookup_plugin_terms(x, variables, loader=self._loader)
|
||||
results.append(intermediate)
|
||||
return results
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
terms = self.__lookup_variabless(terms, variables)
|
||||
|
||||
my_list = terms[:]
|
||||
if len(my_list) == 0:
|
||||
raise AnsibleError("with_cartesian requires at least one element in each list")
|
||||
|
||||
return [self._flatten(x) for x in product(*my_list, fillvalue=None)]
|
||||
|
||||
81
lib/ansible/plugins/lookup/csvfile.py
Normal file
81
lib/ansible/plugins/lookup/csvfile.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# (c) 2013, Jan-Piet Mens <jpmens(at)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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import codecs
|
||||
import csv
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def read_csv(self, filename, key, delimiter, dflt=None, col=1):
|
||||
|
||||
try:
|
||||
f = codecs.open(filename, 'r', encoding='utf-8')
|
||||
creader = csv.reader(f, delimiter=delimiter)
|
||||
|
||||
for row in creader:
|
||||
if row[0] == key:
|
||||
return row[int(col)]
|
||||
except Exception as e:
|
||||
raise AnsibleError("csvfile: %s" % str(e))
|
||||
|
||||
return dflt
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
params = term.split()
|
||||
key = params[0]
|
||||
|
||||
paramvals = {
|
||||
'file' : 'ansible.csv',
|
||||
'default' : None,
|
||||
'delimiter' : "TAB",
|
||||
'col' : "1", # column to return
|
||||
}
|
||||
|
||||
# parameters specified?
|
||||
try:
|
||||
for param in params[1:]:
|
||||
name, value = param.split('=')
|
||||
assert(name in paramvals)
|
||||
paramvals[name] = value
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
|
||||
if paramvals['delimiter'] == 'TAB':
|
||||
paramvals['delimiter'] = "\t"
|
||||
|
||||
path = self._loader.path_dwim(paramvals['file'])
|
||||
|
||||
var = self.read_csv(path, key, paramvals['delimiter'], paramvals['default'], paramvals['col'])
|
||||
if var is not None:
|
||||
if type(var) is list:
|
||||
for v in var:
|
||||
ret.append(v)
|
||||
else:
|
||||
ret.append(var)
|
||||
return ret
|
||||
30
lib/ansible/plugins/lookup/dict.py
Normal file
30
lib/ansible/plugins/lookup/dict.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# (c) 2014, Kent R. Spillner <kspillner@acm.org>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, varibles=None, **kwargs):
|
||||
|
||||
if not isinstance(terms, dict):
|
||||
raise AnsibleError("with_dict expects a dict")
|
||||
|
||||
return self._flatten_hash_to_list(terms)
|
||||
70
lib/ansible/plugins/lookup/dnstxt.py
Normal file
70
lib/ansible/plugins/lookup/dnstxt.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# (c) 2012, Jan-Piet Mens <jpmens(at)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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
HAVE_DNS=False
|
||||
try:
|
||||
import dns.resolver
|
||||
from dns.exception import DNSException
|
||||
HAVE_DNS=True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
# ==============================================================
|
||||
# DNSTXT: DNS TXT records
|
||||
#
|
||||
# key=domainname
|
||||
# TODO: configurable resolver IPs
|
||||
# --------------------------------------------------------------
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if HAVE_DNS == False:
|
||||
raise AnsibleError("Can't LOOKUP(dnstxt): module dns.resolver is not installed")
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
domain = term.split()[0]
|
||||
string = []
|
||||
try:
|
||||
answers = dns.resolver.query(domain, 'TXT')
|
||||
for rdata in answers:
|
||||
s = rdata.to_text()
|
||||
string.append(s[1:-1]) # Strip outside quotes on TXT rdata
|
||||
|
||||
except dns.resolver.NXDOMAIN:
|
||||
string = 'NXDOMAIN'
|
||||
except dns.resolver.Timeout:
|
||||
string = ''
|
||||
except dns.exception.DNSException as e:
|
||||
raise AnsibleError("dns.resolver unhandled exception", e)
|
||||
|
||||
ret.append(''.join(string))
|
||||
|
||||
return ret
|
||||
|
||||
36
lib/ansible/plugins/lookup/env.py
Normal file
36
lib/ansible/plugins/lookup/env.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# (c) 2012, Jan-Piet Mens <jpmens(at)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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
var = term.split()[0]
|
||||
ret.append(os.getenv(var, ''))
|
||||
|
||||
return ret
|
||||
77
lib/ansible/plugins/lookup/etcd.py
Normal file
77
lib/ansible/plugins/lookup/etcd.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# (c) 2013, Jan-Piet Mens <jpmens(at)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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import urllib2
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
# this can be made configurable, not should not use ansible.cfg
|
||||
ANSIBLE_ETCD_URL = 'http://127.0.0.1:4001'
|
||||
if os.getenv('ANSIBLE_ETCD_URL') is not None:
|
||||
ANSIBLE_ETCD_URL = os.environ['ANSIBLE_ETCD_URL']
|
||||
|
||||
class etcd():
|
||||
def __init__(self, url=ANSIBLE_ETCD_URL):
|
||||
self.url = url
|
||||
self.baseurl = '%s/v1/keys' % (self.url)
|
||||
|
||||
def get(self, key):
|
||||
url = "%s/%s" % (self.baseurl, key)
|
||||
|
||||
data = None
|
||||
value = ""
|
||||
try:
|
||||
r = urllib2.urlopen(url)
|
||||
data = r.read()
|
||||
except:
|
||||
return value
|
||||
|
||||
try:
|
||||
# {"action":"get","key":"/name","value":"Jane Jolie","index":5}
|
||||
item = json.loads(data)
|
||||
if 'value' in item:
|
||||
value = item['value']
|
||||
if 'errorCode' in item:
|
||||
value = "ENOENT"
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
|
||||
return value
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
etcd = etcd()
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
key = term.split()[0]
|
||||
value = etcd.get(key)
|
||||
ret.append(value)
|
||||
return ret
|
||||
60
lib/ansible/plugins/lookup/file.py
Normal file
60
lib/ansible/plugins/lookup/file.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import codecs
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
basedir_path = self._loader.path_dwim(term)
|
||||
relative_path = None
|
||||
playbook_path = None
|
||||
|
||||
# Special handling of the file lookup, used primarily when the
|
||||
# lookup is done from a role. If the file isn't found in the
|
||||
# basedir of the current file, use dwim_relative to look in the
|
||||
# role/files/ directory, and finally the playbook directory
|
||||
# itself (which will be relative to the current working dir)
|
||||
|
||||
# FIXME: the original file stuff still needs to be worked out, but the
|
||||
# playbook_dir stuff should be able to be removed as it should
|
||||
# be covered by the fact that the loader contains that info
|
||||
#if '_original_file' in variables:
|
||||
# relative_path = self._loader.path_dwim_relative(variables['_original_file'], 'files', term, self.basedir, check=False)
|
||||
#if 'playbook_dir' in variables:
|
||||
# playbook_path = os.path.join(variables['playbook_dir'], term)
|
||||
|
||||
for path in (basedir_path, relative_path, playbook_path):
|
||||
if path and os.path.exists(path):
|
||||
ret.append(codecs.open(path, encoding="utf8").read().rstrip())
|
||||
break
|
||||
else:
|
||||
raise AnsibleError("could not locate file in lookup: %s" % term)
|
||||
|
||||
return ret
|
||||
34
lib/ansible/plugins/lookup/fileglob.py
Normal file
34
lib/ansible/plugins/lookup/fileglob.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
dwimmed = self._loader.path_dwim(term)
|
||||
globbed = glob.glob(dwimmed)
|
||||
ret.extend(g for g in globbed if os.path.isfile(g))
|
||||
return ret
|
||||
206
lib/ansible/plugins/lookup/first_found.py
Normal file
206
lib/ansible/plugins/lookup/first_found.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# (c) 2013, seth vidal <skvidal@fedoraproject.org> red hat, inc
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# take a list of files and (optionally) a list of paths
|
||||
# return the first existing file found in the paths
|
||||
# [file1, file2, file3], [path1, path2, path3]
|
||||
# search order is:
|
||||
# path1/file1
|
||||
# path1/file2
|
||||
# path1/file3
|
||||
# path2/file1
|
||||
# path2/file2
|
||||
# path2/file3
|
||||
# path3/file1
|
||||
# path3/file2
|
||||
# path3/file3
|
||||
|
||||
# first file found with os.path.exists() is returned
|
||||
# no file matches raises ansibleerror
|
||||
# EXAMPLES
|
||||
# - name: copy first existing file found to /some/file
|
||||
# action: copy src=$item dest=/some/file
|
||||
# with_first_found:
|
||||
# - files: foo ${inventory_hostname} bar
|
||||
# paths: /tmp/production /tmp/staging
|
||||
|
||||
# that will look for files in this order:
|
||||
# /tmp/production/foo
|
||||
# ${inventory_hostname}
|
||||
# bar
|
||||
# /tmp/staging/foo
|
||||
# ${inventory_hostname}
|
||||
# bar
|
||||
|
||||
# - name: copy first existing file found to /some/file
|
||||
# action: copy src=$item dest=/some/file
|
||||
# with_first_found:
|
||||
# - files: /some/place/foo ${inventory_hostname} /some/place/else
|
||||
|
||||
# that will look for files in this order:
|
||||
# /some/place/foo
|
||||
# $relative_path/${inventory_hostname}
|
||||
# /some/place/else
|
||||
|
||||
# example - including tasks:
|
||||
# tasks:
|
||||
# - include: $item
|
||||
# with_first_found:
|
||||
# - files: generic
|
||||
# paths: tasks/staging tasks/production
|
||||
# this will include the tasks in the file generic where it is found first (staging or production)
|
||||
|
||||
# example simple file lists
|
||||
#tasks:
|
||||
#- name: first found file
|
||||
# action: copy src=$item dest=/etc/file.cfg
|
||||
# with_first_found:
|
||||
# - files: foo.${inventory_hostname} foo
|
||||
|
||||
|
||||
# example skipping if no matched files
|
||||
# First_found also offers the ability to control whether or not failing
|
||||
# to find a file returns an error or not
|
||||
#
|
||||
#- name: first found file - or skip
|
||||
# action: copy src=$item dest=/etc/file.cfg
|
||||
# with_first_found:
|
||||
# - files: foo.${inventory_hostname}
|
||||
# skip: true
|
||||
|
||||
# example a role with default configuration and configuration per host
|
||||
# you can set multiple terms with their own files and paths to look through.
|
||||
# consider a role that sets some configuration per host falling back on a default config.
|
||||
#
|
||||
#- name: some configuration template
|
||||
# template: src={{ item }} dest=/etc/file.cfg mode=0444 owner=root group=root
|
||||
# with_first_found:
|
||||
# - files:
|
||||
# - ${inventory_hostname}/etc/file.cfg
|
||||
# paths:
|
||||
# - ../../../templates.overwrites
|
||||
# - ../../../templates
|
||||
# - files:
|
||||
# - etc/file.cfg
|
||||
# paths:
|
||||
# - templates
|
||||
|
||||
# the above will return an empty list if the files cannot be found at all
|
||||
# if skip is unspecificed or if it is set to false then it will return a list
|
||||
# error which can be caught bye ignore_errors: true for that action.
|
||||
|
||||
# finally - if you want you can use it, in place to replace first_available_file:
|
||||
# you simply cannot use the - files, path or skip options. simply replace
|
||||
# first_available_file with with_first_found and leave the file listing in place
|
||||
#
|
||||
#
|
||||
# - name: with_first_found like first_available_file
|
||||
# action: copy src=$item dest=/tmp/faftest
|
||||
# with_first_found:
|
||||
# - ../files/foo
|
||||
# - ../files/bar
|
||||
# - ../files/baz
|
||||
# ignore_errors: true
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from jinja2.exceptions import UndefinedError
|
||||
|
||||
from ansible.errors import AnsibleUndefinedVariable
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.template import Templar
|
||||
from ansible.utils.boolean import boolean
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
result = None
|
||||
anydict = False
|
||||
skip = False
|
||||
|
||||
for term in terms:
|
||||
if isinstance(term, dict):
|
||||
anydict = True
|
||||
|
||||
total_search = []
|
||||
if anydict:
|
||||
for term in terms:
|
||||
if isinstance(term, dict):
|
||||
files = term.get('files', [])
|
||||
paths = term.get('paths', [])
|
||||
skip = boolean(term.get('skip', False))
|
||||
|
||||
filelist = files
|
||||
if isinstance(files, basestring):
|
||||
files = files.replace(',', ' ')
|
||||
files = files.replace(';', ' ')
|
||||
filelist = files.split(' ')
|
||||
|
||||
pathlist = paths
|
||||
if paths:
|
||||
if isinstance(paths, basestring):
|
||||
paths = paths.replace(',', ' ')
|
||||
paths = paths.replace(':', ' ')
|
||||
paths = paths.replace(';', ' ')
|
||||
pathlist = paths.split(' ')
|
||||
|
||||
if not pathlist:
|
||||
total_search = filelist
|
||||
else:
|
||||
for path in pathlist:
|
||||
for fn in filelist:
|
||||
f = os.path.join(path, fn)
|
||||
total_search.append(f)
|
||||
else:
|
||||
total_search.append(term)
|
||||
else:
|
||||
total_search = terms
|
||||
|
||||
templar = Templar(loader=self._loader, variables=variables)
|
||||
roledir = variables.get('roledir')
|
||||
for fn in total_search:
|
||||
try:
|
||||
fn = templar.template(fn)
|
||||
except (AnsibleUndefinedVariable, UndefinedError) as e:
|
||||
continue
|
||||
|
||||
if os.path.isabs(fn) and os.path.exists(fn):
|
||||
return [fn]
|
||||
else:
|
||||
if roledir is not None:
|
||||
# check the templates and vars directories too,if they exist
|
||||
for subdir in ('templates', 'vars'):
|
||||
path = self._loader.path_dwim_relative(roledir, subdir, fn)
|
||||
if os.path.exists(path):
|
||||
return [path]
|
||||
|
||||
# if none of the above were found, just check the
|
||||
# current filename against the basedir (this will already
|
||||
# have ../files from runner, if it's a role task
|
||||
path = self._loader.path_dwim(fn)
|
||||
if os.path.exists(path):
|
||||
return [path]
|
||||
else:
|
||||
if skip:
|
||||
return []
|
||||
else:
|
||||
return [None]
|
||||
|
||||
70
lib/ansible/plugins/lookup/flattened.py
Normal file
70
lib/ansible/plugins/lookup/flattened.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# (c) 2013, Serge van Ginderachter <serge@vanginderachter.be>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def _check_list_of_one_list(self, term):
|
||||
# make sure term is not a list of one (list of one..) item
|
||||
# return the final non list item if so
|
||||
|
||||
if isinstance(term,list) and len(term) == 1:
|
||||
term = term[0]
|
||||
if isinstance(term,list):
|
||||
term = self._check_list_of_one_list(term)
|
||||
|
||||
return term
|
||||
|
||||
def _do_flatten(self, terms, variables):
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
term = self._check_list_of_one_list(term)
|
||||
|
||||
if term == 'None' or term == 'null':
|
||||
# ignore undefined items
|
||||
break
|
||||
|
||||
if isinstance(term, basestring):
|
||||
# convert a variable to a list
|
||||
term2 = listify_lookup_plugin_terms(term, variables, loader=self._loader)
|
||||
# but avoid converting a plain string to a list of one string
|
||||
if term2 != [ term ]:
|
||||
term = term2
|
||||
|
||||
if isinstance(term, list):
|
||||
# if it's a list, check recursively for items that are a list
|
||||
term = self._do_flatten(term, variables)
|
||||
ret.extend(term)
|
||||
else:
|
||||
ret.append(term)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
raise AnsibleError("with_flattened expects a list")
|
||||
|
||||
return self._do_flatten(terms, variables)
|
||||
|
||||
35
lib/ansible/plugins/lookup/indexed_items.py
Normal file
35
lib/ansible/plugins/lookup/indexed_items.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def __init__(self, basedir=None, **kwargs):
|
||||
self.basedir = basedir
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
raise AnsibleError("with_indexed_items expects a list")
|
||||
|
||||
items = self._flatten(terms)
|
||||
return zip(range(len(items)), items)
|
||||
|
||||
37
lib/ansible/plugins/lookup/inventory_hostnames.py
Normal file
37
lib/ansible/plugins/lookup/inventory_hostnames.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2013, Steven Dossett <sdossett@panath.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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, inject=None, **kwargs):
|
||||
if not isinstance(terms, list):
|
||||
raise AnsibleError("with_inventory_hostnames expects a list")
|
||||
|
||||
# FIXME: the inventory is no longer available this way, so we may have
|
||||
# to dump the host list into the list of variables and read it back
|
||||
# in here (or the inventory sources, so we can recreate the list
|
||||
# of hosts)
|
||||
#return self._flatten(inventory.Inventory(self.host_list).list_hosts(terms))
|
||||
return terms
|
||||
|
||||
30
lib/ansible/plugins/lookup/items.py
Normal file
30
lib/ansible/plugins/lookup/items.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
terms = [ terms ]
|
||||
|
||||
return self._flatten(terms)
|
||||
|
||||
37
lib/ansible/plugins/lookup/lines.py
Normal file
37
lib/ansible/plugins/lookup/lines.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.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/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import subprocess
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
p = subprocess.Popen(term, cwd=self._loader.get_basedir(), shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode == 0:
|
||||
ret.extend(stdout.splitlines())
|
||||
else:
|
||||
raise AnsibleError("lookup_plugin.lines(%s) returned %d" % (term, p.returncode))
|
||||
return ret
|
||||
51
lib/ansible/plugins/lookup/nested.py
Normal file
51
lib/ansible/plugins/lookup/nested.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def __lookup_variabless(self, terms, variables):
|
||||
results = []
|
||||
for x in terms:
|
||||
intermediate = listify_lookup_plugin_terms(x, variables, loader=self._loader)
|
||||
results.append(intermediate)
|
||||
return results
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
terms = self.__lookup_variabless(terms, variables)
|
||||
|
||||
my_list = terms[:]
|
||||
my_list.reverse()
|
||||
result = []
|
||||
if len(my_list) == 0:
|
||||
raise AnsibleError("with_nested requires at least one element in the nested list")
|
||||
result = my_list.pop()
|
||||
while len(my_list) > 0:
|
||||
result2 = self._combine(result, my_list.pop())
|
||||
result = result2
|
||||
new_result = []
|
||||
for x in result:
|
||||
new_result.append(self._flatten(x))
|
||||
return new_result
|
||||
|
||||
|
||||
148
lib/ansible/plugins/lookup/password.py
Normal file
148
lib/ansible/plugins/lookup/password.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
|
||||
# (c) 2013, Javier Candeira <javier@candeira.com>
|
||||
# (c) 2013, Maykel Moya <mmoya@speedyrails.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import errno
|
||||
import string
|
||||
import random
|
||||
|
||||
from string import ascii_letters, digits
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.encrypt import do_encrypt
|
||||
|
||||
DEFAULT_LENGTH = 20
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def random_password(self, length=DEFAULT_LENGTH, chars=C.DEFAULT_PASSWORD_CHARS):
|
||||
'''
|
||||
Return a random password string of length containing only chars.
|
||||
NOTE: this was moved from the old ansible utils code, as nothing
|
||||
else appeared to use it.
|
||||
'''
|
||||
|
||||
password = []
|
||||
while len(password) < length:
|
||||
new_char = os.urandom(1)
|
||||
if new_char in chars:
|
||||
password.append(new_char)
|
||||
|
||||
return ''.join(password)
|
||||
|
||||
def random_salt(self):
|
||||
salt_chars = ascii_letters + digits + './'
|
||||
return self.random_password(length=8, chars=salt_chars)
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
ret = []
|
||||
|
||||
if not isinstance(terms, list):
|
||||
terms = [ terms ]
|
||||
|
||||
for term in terms:
|
||||
# you can't have escaped spaces in yor pathname
|
||||
params = term.split()
|
||||
relpath = params[0]
|
||||
|
||||
paramvals = {
|
||||
'length': DEFAULT_LENGTH,
|
||||
'encrypt': None,
|
||||
'chars': ['ascii_letters','digits',".,:-_"],
|
||||
}
|
||||
|
||||
# get non-default parameters if specified
|
||||
try:
|
||||
for param in params[1:]:
|
||||
name, value = param.split('=')
|
||||
assert(name in paramvals)
|
||||
if name == 'length':
|
||||
paramvals[name] = int(value)
|
||||
elif name == 'chars':
|
||||
use_chars=[]
|
||||
if ",," in value:
|
||||
use_chars.append(',')
|
||||
use_chars.extend(value.replace(',,',',').split(','))
|
||||
paramvals['chars'] = use_chars
|
||||
else:
|
||||
paramvals[name] = value
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
|
||||
length = paramvals['length']
|
||||
encrypt = paramvals['encrypt']
|
||||
use_chars = paramvals['chars']
|
||||
|
||||
# get password or create it if file doesn't exist
|
||||
path = self._loader.path_dwim(relpath)
|
||||
if not os.path.exists(path):
|
||||
pathdir = os.path.dirname(path)
|
||||
if not os.path.isdir(pathdir):
|
||||
try:
|
||||
os.makedirs(pathdir, mode=0o700)
|
||||
except OSError as e:
|
||||
raise AnsibleError("cannot create the path for the password lookup: %s (error was %s)" % (pathdir, str(e)))
|
||||
|
||||
chars = "".join([getattr(string,c,c) for c in use_chars]).replace('"','').replace("'",'')
|
||||
password = ''.join(random.choice(chars) for _ in range(length))
|
||||
|
||||
if encrypt is not None:
|
||||
salt = self.random_salt()
|
||||
content = '%s salt=%s' % (password, salt)
|
||||
else:
|
||||
content = password
|
||||
with open(path, 'w') as f:
|
||||
os.chmod(path, 0o600)
|
||||
f.write(content + '\n')
|
||||
else:
|
||||
content = open(path).read().rstrip()
|
||||
sep = content.find(' ')
|
||||
|
||||
if sep >= 0:
|
||||
password = content[:sep]
|
||||
salt = content[sep+1:].split('=')[1]
|
||||
else:
|
||||
password = content
|
||||
salt = None
|
||||
|
||||
# crypt requested, add salt if missing
|
||||
if (encrypt is not None and not salt):
|
||||
salt = self.random_salt()
|
||||
content = '%s salt=%s' % (password, salt)
|
||||
with open(path, 'w') as f:
|
||||
os.chmod(path, 0o600)
|
||||
f.write(content + '\n')
|
||||
# crypt not requested, remove salt if present
|
||||
elif (encrypt is None and salt):
|
||||
with open(path, 'w') as f:
|
||||
os.chmod(path, 0o600)
|
||||
f.write(password + '\n')
|
||||
|
||||
if encrypt:
|
||||
password = do_encrypt(password, encrypt, salt=salt)
|
||||
|
||||
ret.append(password)
|
||||
|
||||
return ret
|
||||
|
||||
51
lib/ansible/plugins/lookup/pipe.py
Normal file
51
lib/ansible/plugins/lookup/pipe.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import subprocess
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
'''
|
||||
http://docs.python.org/2/library/subprocess.html#popen-constructor
|
||||
|
||||
The shell argument (which defaults to False) specifies whether to use the
|
||||
shell as the program to execute. If shell is True, it is recommended to pass
|
||||
args as a string rather than as a sequence
|
||||
|
||||
https://github.com/ansible/ansible/issues/6550
|
||||
'''
|
||||
term = str(term)
|
||||
|
||||
p = subprocess.Popen(term, cwd=self._loader.get_basedir(), shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode == 0:
|
||||
ret.append(stdout.decode("utf-8").rstrip())
|
||||
else:
|
||||
raise AnsibleError("lookup_plugin.pipe(%s) returned %d" % (term, p.returncode))
|
||||
return ret
|
||||
39
lib/ansible/plugins/lookup/random_choice.py
Normal file
39
lib/ansible/plugins/lookup/random_choice.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# (c) 2013, 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import random
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
# useful for introducing chaos ... or just somewhat reasonably fair selection
|
||||
# amongst available mirrors
|
||||
#
|
||||
# tasks:
|
||||
# - debug: msg=$item
|
||||
# with_random_choice:
|
||||
# - one
|
||||
# - two
|
||||
# - three
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, inject=None, **kwargs):
|
||||
|
||||
return [ random.choice(terms) ]
|
||||
|
||||
75
lib/ansible/plugins/lookup/redis_kv.py
Normal file
75
lib/ansible/plugins/lookup/redis_kv.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# (c) 2012, Jan-Piet Mens <jpmens(at)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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
HAVE_REDIS=False
|
||||
try:
|
||||
import redis # https://github.com/andymccurdy/redis-py/
|
||||
HAVE_REDIS=True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
# ==============================================================
|
||||
# REDISGET: Obtain value from a GET on a Redis key. Terms
|
||||
# expected: 0 = URL, 1 = Key
|
||||
# URL may be empty, in which case redis://localhost:6379 assumed
|
||||
# --------------------------------------------------------------
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not HAVE_REDIS:
|
||||
raise AnsibleError("Can't LOOKUP(redis_kv): module redis is not installed")
|
||||
|
||||
if not isinstance(terms, list):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
(url,key) = term.split(',')
|
||||
if url == "":
|
||||
url = 'redis://localhost:6379'
|
||||
|
||||
# urlsplit on Python 2.6.1 is broken. Hmm. Probably also the reason
|
||||
# Redis' from_url() doesn't work here.
|
||||
|
||||
p = '(?P<scheme>[^:]+)://?(?P<host>[^:/ ]+).?(?P<port>[0-9]*).*'
|
||||
|
||||
try:
|
||||
m = re.search(p, url)
|
||||
host = m.group('host')
|
||||
port = int(m.group('port'))
|
||||
except AttributeError:
|
||||
raise AnsibleError("Bad URI in redis lookup")
|
||||
|
||||
try:
|
||||
conn = redis.Redis(host=host, port=port)
|
||||
res = conn.get(key)
|
||||
if res is None:
|
||||
res = ""
|
||||
ret.append(res)
|
||||
except:
|
||||
ret.append("") # connection failed or key not found
|
||||
return ret
|
||||
203
lib/ansible/plugins/lookup/sequence.py
Normal file
203
lib/ansible/plugins/lookup/sequence.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# (c) 2013, Jayson Vantuyl <jayson@aggressive.ly>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from re import compile as re_compile, IGNORECASE
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.template import Templar
|
||||
|
||||
# shortcut format
|
||||
NUM = "(0?x?[0-9a-f]+)"
|
||||
SHORTCUT = re_compile(
|
||||
"^(" + # Group 0
|
||||
NUM + # Group 1: Start
|
||||
"-)?" +
|
||||
NUM + # Group 2: End
|
||||
"(/" + # Group 3
|
||||
NUM + # Group 4: Stride
|
||||
")?" +
|
||||
"(:(.+))?$", # Group 5, Group 6: Format String
|
||||
IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
sequence lookup module
|
||||
|
||||
Used to generate some sequence of items. Takes arguments in two forms.
|
||||
|
||||
The simple / shortcut form is:
|
||||
|
||||
[start-]end[/stride][:format]
|
||||
|
||||
As indicated by the brackets: start, stride, and format string are all
|
||||
optional. The format string is in the style of printf. This can be used
|
||||
to pad with zeros, format in hexadecimal, etc. All of the numerical values
|
||||
can be specified in octal (i.e. 0664) or hexadecimal (i.e. 0x3f8).
|
||||
Negative numbers are not supported.
|
||||
|
||||
Some examples:
|
||||
|
||||
5 -> ["1","2","3","4","5"]
|
||||
5-8 -> ["5", "6", "7", "8"]
|
||||
2-10/2 -> ["2", "4", "6", "8", "10"]
|
||||
4:host%02d -> ["host01","host02","host03","host04"]
|
||||
|
||||
The standard Ansible key-value form is accepted as well. For example:
|
||||
|
||||
start=5 end=11 stride=2 format=0x%02x -> ["0x05","0x07","0x09","0x0a"]
|
||||
|
||||
This format takes an alternate form of "end" called "count", which counts
|
||||
some number from the starting value. For example:
|
||||
|
||||
count=5 -> ["1", "2", "3", "4", "5"]
|
||||
start=0x0f00 count=4 format=%04x -> ["0f00", "0f01", "0f02", "0f03"]
|
||||
start=0 count=5 stride=2 -> ["0", "2", "4", "6", "8"]
|
||||
start=1 count=5 stride=2 -> ["1", "3", "5", "7", "9"]
|
||||
|
||||
The count option is mostly useful for avoiding off-by-one errors and errors
|
||||
calculating the number of entries in a sequence when a stride is specified.
|
||||
"""
|
||||
|
||||
def reset(self):
|
||||
"""set sensible defaults"""
|
||||
self.start = 1
|
||||
self.count = None
|
||||
self.end = None
|
||||
self.stride = 1
|
||||
self.format = "%d"
|
||||
|
||||
def parse_kv_args(self, args):
|
||||
"""parse key-value style arguments"""
|
||||
for arg in ["start", "end", "count", "stride"]:
|
||||
try:
|
||||
arg_raw = args.pop(arg, None)
|
||||
if arg_raw is None:
|
||||
continue
|
||||
arg_cooked = int(arg_raw, 0)
|
||||
setattr(self, arg, arg_cooked)
|
||||
except ValueError:
|
||||
raise AnsibleError(
|
||||
"can't parse arg %s=%r as integer"
|
||||
% (arg, arg_raw)
|
||||
)
|
||||
if 'format' in args:
|
||||
self.format = args.pop("format")
|
||||
if args:
|
||||
raise AnsibleError(
|
||||
"unrecognized arguments to with_sequence: %r"
|
||||
% args.keys()
|
||||
)
|
||||
|
||||
def parse_simple_args(self, term):
|
||||
"""parse the shortcut forms, return True/False"""
|
||||
match = SHORTCUT.match(term)
|
||||
if not match:
|
||||
return False
|
||||
|
||||
_, start, end, _, stride, _, format = match.groups()
|
||||
|
||||
if start is not None:
|
||||
try:
|
||||
start = int(start, 0)
|
||||
except ValueError:
|
||||
raise AnsibleError("can't parse start=%s as integer" % start)
|
||||
if end is not None:
|
||||
try:
|
||||
end = int(end, 0)
|
||||
except ValueError:
|
||||
raise AnsibleError("can't parse end=%s as integer" % end)
|
||||
if stride is not None:
|
||||
try:
|
||||
stride = int(stride, 0)
|
||||
except ValueError:
|
||||
raise AnsibleError("can't parse stride=%s as integer" % stride)
|
||||
|
||||
if start is not None:
|
||||
self.start = start
|
||||
if end is not None:
|
||||
self.end = end
|
||||
if stride is not None:
|
||||
self.stride = stride
|
||||
if format is not None:
|
||||
self.format = format
|
||||
|
||||
def sanity_check(self):
|
||||
if self.count is None and self.end is None:
|
||||
raise AnsibleError(
|
||||
"must specify count or end in with_sequence"
|
||||
)
|
||||
elif self.count is not None and self.end is not None:
|
||||
raise AnsibleError(
|
||||
"can't specify both count and end in with_sequence"
|
||||
)
|
||||
elif self.count is not None:
|
||||
# convert count to end
|
||||
self.end = self.start + self.count * self.stride - 1
|
||||
del self.count
|
||||
if self.end < self.start:
|
||||
raise AnsibleError("can't count backwards")
|
||||
if self.format.count('%') != 1:
|
||||
raise AnsibleError("bad formatting string: %s" % self.format)
|
||||
|
||||
def generate_sequence(self):
|
||||
numbers = xrange(self.start, self.end + 1, self.stride)
|
||||
|
||||
for i in numbers:
|
||||
try:
|
||||
formatted = self.format % i
|
||||
yield formatted
|
||||
except (ValueError, TypeError):
|
||||
raise AnsibleError(
|
||||
"problem formatting %r with %r" % self.format
|
||||
)
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
results = []
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
templar = Templar(loader=self._loader, variables=variables)
|
||||
|
||||
for term in terms:
|
||||
try:
|
||||
self.reset() # clear out things for this iteration
|
||||
|
||||
term = templar.template(term)
|
||||
try:
|
||||
if not self.parse_simple_args(term):
|
||||
self.parse_kv_args(parse_kv(term))
|
||||
except Exception, e:
|
||||
raise AnsibleError("unknown error parsing with_sequence arguments: %r. Error was: %s" % (term, e))
|
||||
|
||||
self.sanity_check()
|
||||
|
||||
results.extend(self.generate_sequence())
|
||||
except AnsibleError:
|
||||
raise
|
||||
except Exception:
|
||||
raise AnsibleError(
|
||||
"unknown error generating sequence"
|
||||
)
|
||||
|
||||
return results
|
||||
61
lib/ansible/plugins/lookup/subelements.py
Normal file
61
lib/ansible/plugins/lookup/subelements.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# (c) 2013, Serge van Ginderachter <serge@vanginderachter.be>
|
||||
#
|
||||
# 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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
terms[0] = listify_lookup_plugin_terms(terms[0], variables, loader=self._loader)
|
||||
|
||||
if not isinstance(terms, list) or not len(terms) == 2:
|
||||
raise AnsibleError("subelements lookup expects a list of two items, first a dict or a list, and second a string")
|
||||
|
||||
if isinstance(terms[0], dict): # convert to list:
|
||||
if terms[0].get('skipped',False) != False:
|
||||
# the registered result was completely skipped
|
||||
return []
|
||||
elementlist = []
|
||||
for key in terms[0].iterkeys():
|
||||
elementlist.append(terms[0][key])
|
||||
else:
|
||||
elementlist = terms[0]
|
||||
|
||||
subelement = terms[1]
|
||||
|
||||
ret = []
|
||||
for item0 in elementlist:
|
||||
if not isinstance(item0, dict):
|
||||
raise AnsibleError("subelements lookup expects a dictionary, got '%s'" %item0)
|
||||
if item0.get('skipped', False) != False:
|
||||
# this particular item is to be skipped
|
||||
continue
|
||||
if not subelement in item0:
|
||||
raise AnsibleError("could not find '%s' key in iterated item '%s'" % (subelement, item0))
|
||||
if not isinstance(item0[subelement], list):
|
||||
raise AnsibleError("the key %s should point to a list, got '%s'" % (subelement, item0[subelement]))
|
||||
sublist = item0.pop(subelement, [])
|
||||
for item1 in sublist:
|
||||
ret.append((item0, item1))
|
||||
|
||||
return ret
|
||||
|
||||
45
lib/ansible/plugins/lookup/template.py
Normal file
45
lib/ansible/plugins/lookup/template.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# (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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.template import Templar
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
|
||||
if not isinstance(terms, list):
|
||||
terms = [ terms ]
|
||||
|
||||
templar = Templar(loader=self._loader, variables=variables)
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
path = self._loader.path_dwim(term)
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
template_data = f.read()
|
||||
res = templar.template(template_data, preserve_trailing_newlines=True)
|
||||
ret.append(res)
|
||||
else:
|
||||
raise AnsibleError("the template file %s could not be found for the lookup" % term)
|
||||
return ret
|
||||
50
lib/ansible/plugins/lookup/together.py
Normal file
50
lib/ansible/plugins/lookup/together.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# (c) 2013, Bradley Young <young.bradley@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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from itertools import izip_longest
|
||||
|
||||
from ansible.errors import *
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
"""
|
||||
Transpose a list of arrays:
|
||||
[1, 2, 3], [4, 5, 6] -> [1, 4], [2, 5], [3, 6]
|
||||
Replace any empty spots in 2nd array with None:
|
||||
[1, 2], [3] -> [1, 3], [2, None]
|
||||
"""
|
||||
|
||||
def __lookup_variabless(self, terms, variables):
|
||||
results = []
|
||||
for x in terms:
|
||||
intermediate = listify_lookup_plugin_terms(x, variables, loader=self._loader)
|
||||
results.append(intermediate)
|
||||
return results
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
|
||||
terms = self.__lookup_variabless(terms, variables)
|
||||
|
||||
my_list = terms[:]
|
||||
if len(my_list) == 0:
|
||||
raise errors.AnsibleError("with_together requires at least one element in each list")
|
||||
|
||||
return [self._flatten(x) for x in izip_longest(*my_list, fillvalue=None)]
|
||||
|
||||
46
lib/ansible/plugins/lookup/url.py
Normal file
46
lib/ansible/plugins/lookup/url.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# (c) 2015, Brian Coca <bcoca@ansible.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
import urllib2
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
|
||||
def run(self, terms, inject=None, **kwargs):
|
||||
|
||||
if isinstance(terms, basestring):
|
||||
terms = [ terms ]
|
||||
|
||||
ret = []
|
||||
for term in terms:
|
||||
try:
|
||||
r = urllib2.Request(term)
|
||||
response = urllib2.urlopen(r)
|
||||
except URLError as e:
|
||||
utils.warnings("Failed lookup url for %s : %s" % (term, str(e)))
|
||||
continue
|
||||
except HTTPError as e:
|
||||
utils.warnings("Received HTTP error for %s : %s" % (term, str(e)))
|
||||
continue
|
||||
|
||||
for line in response.read().splitlines():
|
||||
ret.append(line)
|
||||
|
||||
return ret
|
||||
21
lib/ansible/plugins/shell/__init__.py
Normal file
21
lib/ansible/plugins/shell/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
28
lib/ansible/plugins/shell/csh.py
Normal file
28
lib/ansible/plugins/shell/csh.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.shell.sh import ShellModule as ShModule
|
||||
|
||||
class ShellModule(ShModule):
|
||||
|
||||
# How to end lines in a python script one-liner
|
||||
_SHELL_EMBEDDED_PY_EOL = '\\\n'
|
||||
|
||||
def env_prefix(self, **kwargs):
|
||||
return 'env %s' % super(ShellModule, self).env_prefix(**kwargs)
|
||||
25
lib/ansible/plugins/shell/fish.py
Normal file
25
lib/ansible/plugins/shell/fish.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.shell.sh import ShellModule as ShModule
|
||||
|
||||
class ShellModule(ShModule):
|
||||
|
||||
def env_prefix(self, **kwargs):
|
||||
return 'env %s' % super(ShellModule, self).env_prefix(**kwargs)
|
||||
120
lib/ansible/plugins/shell/powershell.py
Normal file
120
lib/ansible/plugins/shell/powershell.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
import shlex
|
||||
import time
|
||||
|
||||
_common_args = ['PowerShell', '-NoProfile', '-NonInteractive']
|
||||
|
||||
# Primarily for testing, allow explicitly specifying PowerShell version via
|
||||
# an environment variable.
|
||||
_powershell_version = os.environ.get('POWERSHELL_VERSION', None)
|
||||
if _powershell_version:
|
||||
_common_args = ['PowerShell', '-Version', _powershell_version] + _common_args[1:]
|
||||
|
||||
class ShellModule(object):
|
||||
|
||||
def env_prefix(self, **kwargs):
|
||||
return ''
|
||||
|
||||
def join_path(self, *args):
|
||||
return os.path.join(*args).replace('/', '\\')
|
||||
|
||||
def path_has_trailing_slash(self, path):
|
||||
# Allow Windows paths to be specified using either slash.
|
||||
return path.endswith('/') or path.endswith('\\')
|
||||
|
||||
def chmod(self, mode, path):
|
||||
return ''
|
||||
|
||||
def remove(self, path, recurse=False):
|
||||
path = self._escape(path)
|
||||
if recurse:
|
||||
return self._encode_script('''Remove-Item "%s" -Force -Recurse;''' % path)
|
||||
else:
|
||||
return self._encode_script('''Remove-Item "%s" -Force;''' % path)
|
||||
|
||||
def mkdtemp(self, basefile, system=False, mode=None):
|
||||
basefile = self._escape(basefile)
|
||||
# FIXME: Support system temp path!
|
||||
return self._encode_script('''(New-Item -Type Directory -Path $env:temp -Name "%s").FullName | Write-Host -Separator '';''' % basefile)
|
||||
|
||||
def md5(self, path):
|
||||
path = self._escape(path)
|
||||
script = '''
|
||||
If (Test-Path -PathType Leaf "%(path)s")
|
||||
{
|
||||
$sp = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider;
|
||||
$fp = [System.IO.File]::Open("%(path)s", [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read);
|
||||
[System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
|
||||
$fp.Dispose();
|
||||
}
|
||||
ElseIf (Test-Path -PathType Container "%(path)s")
|
||||
{
|
||||
Write-Host "3";
|
||||
}
|
||||
Else
|
||||
{
|
||||
Write-Host "1";
|
||||
}
|
||||
''' % dict(path=path)
|
||||
return self._encode_script(script)
|
||||
|
||||
def build_module_command(self, env_string, shebang, cmd, rm_tmp=None):
|
||||
cmd = cmd.encode('utf-8')
|
||||
cmd_parts = shlex.split(cmd, posix=False)
|
||||
if not cmd_parts[0].lower().endswith('.ps1'):
|
||||
cmd_parts[0] = '%s.ps1' % cmd_parts[0]
|
||||
script = self._build_file_cmd(cmd_parts)
|
||||
if rm_tmp:
|
||||
rm_tmp = self._escape(rm_tmp)
|
||||
script = '%s; Remove-Item "%s" -Force -Recurse;' % (script, rm_tmp)
|
||||
return self._encode_script(script)
|
||||
|
||||
def _escape(self, value, include_vars=False):
|
||||
'''Return value escaped for use in PowerShell command.'''
|
||||
# http://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences
|
||||
# http://stackoverflow.com/questions/764360/a-list-of-string-replacements-in-python
|
||||
subs = [('\n', '`n'), ('\r', '`r'), ('\t', '`t'), ('\a', '`a'),
|
||||
('\b', '`b'), ('\f', '`f'), ('\v', '`v'), ('"', '`"'),
|
||||
('\'', '`\''), ('`', '``'), ('\x00', '`0')]
|
||||
if include_vars:
|
||||
subs.append(('$', '`$'))
|
||||
pattern = '|'.join('(%s)' % re.escape(p) for p, s in subs)
|
||||
substs = [s for p, s in subs]
|
||||
replace = lambda m: substs[m.lastindex - 1]
|
||||
return re.sub(pattern, replace, value)
|
||||
|
||||
def _encode_script(self, script, as_list=False):
|
||||
'''Convert a PowerShell script to a single base64-encoded command.'''
|
||||
script = '\n'.join([x.strip() for x in script.splitlines() if x.strip()])
|
||||
encoded_script = base64.b64encode(script.encode('utf-16-le'))
|
||||
cmd_parts = _common_args + ['-EncodedCommand', encoded_script]
|
||||
if as_list:
|
||||
return cmd_parts
|
||||
return ' '.join(cmd_parts)
|
||||
|
||||
def _build_file_cmd(self, cmd_parts):
|
||||
'''Build command line to run a file, given list of file name plus args.'''
|
||||
return ' '.join(_common_args + ['-ExecutionPolicy', 'Unrestricted', '-File'] + ['"%s"' % x for x in cmd_parts])
|
||||
|
||||
133
lib/ansible/plugins/shell/sh.py
Normal file
133
lib/ansible/plugins/shell/sh.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import re
|
||||
import pipes
|
||||
import ansible.constants as C
|
||||
import time
|
||||
import random
|
||||
|
||||
_USER_HOME_PATH_RE = re.compile(r'^~[_.A-Za-z0-9][-_.A-Za-z0-9]*$')
|
||||
|
||||
class ShellModule(object):
|
||||
|
||||
# How to end lines in a python script one-liner
|
||||
_SHELL_EMBEDDED_PY_EOL = '\n'
|
||||
|
||||
def env_prefix(self, **kwargs):
|
||||
'''Build command prefix with environment variables.'''
|
||||
env = dict(
|
||||
LANG = C.DEFAULT_MODULE_LANG,
|
||||
LC_CTYPE = C.DEFAULT_MODULE_LANG,
|
||||
)
|
||||
env.update(kwargs)
|
||||
return ' '.join(['%s=%s' % (k, pipes.quote(unicode(v))) for k,v in env.items()])
|
||||
|
||||
def join_path(self, *args):
|
||||
return os.path.join(*args)
|
||||
|
||||
def path_has_trailing_slash(self, path):
|
||||
return path.endswith('/')
|
||||
|
||||
def chmod(self, mode, path):
|
||||
path = pipes.quote(path)
|
||||
return 'chmod %s %s' % (mode, path)
|
||||
|
||||
def remove(self, path, recurse=False):
|
||||
path = pipes.quote(path)
|
||||
if recurse:
|
||||
return "rm -rf %s >/dev/null 2>&1" % path
|
||||
else:
|
||||
return "rm -f %s >/dev/null 2>&1" % path
|
||||
|
||||
def mkdtemp(self, basefile=None, system=False, mode=None):
|
||||
if not basefile:
|
||||
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
|
||||
basetmp = self.join_path(C.DEFAULT_REMOTE_TMP, basefile)
|
||||
if system and basetmp.startswith('$HOME'):
|
||||
basetmp = self.join_path('/tmp', basefile)
|
||||
cmd = 'mkdir -p %s' % basetmp
|
||||
if mode:
|
||||
cmd += ' && chmod %s %s' % (mode, basetmp)
|
||||
cmd += ' && echo %s' % basetmp
|
||||
return cmd
|
||||
|
||||
def expand_user(self, user_home_path):
|
||||
''' Return a command to expand tildes in a path
|
||||
|
||||
It can be either "~" or "~username". We use the POSIX definition of
|
||||
a username:
|
||||
http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap03.html#tag_03_426
|
||||
http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap03.html#tag_03_276
|
||||
'''
|
||||
|
||||
# Check that the user_path to expand is safe
|
||||
if user_home_path != '~':
|
||||
if not _USER_HOME_PATH_RE.match(user_home_path):
|
||||
# pipes.quote will make the shell return the string verbatim
|
||||
user_home_path = pipes.quote(user_home_path)
|
||||
return 'echo %s' % user_home_path
|
||||
|
||||
def checksum(self, path, python_interp):
|
||||
# The following test needs to be SH-compliant. BASH-isms will
|
||||
# not work if /bin/sh points to a non-BASH shell.
|
||||
#
|
||||
# In the following test, each condition is a check and logical
|
||||
# comparison (|| or &&) that sets the rc value. Every check is run so
|
||||
# the last check in the series to fail will be the rc that is
|
||||
# returned.
|
||||
#
|
||||
# If a check fails we error before invoking the hash functions because
|
||||
# hash functions may successfully take the hash of a directory on BSDs
|
||||
# (UFS filesystem?) which is not what the rest of the ansible code
|
||||
# expects
|
||||
#
|
||||
# If all of the available hashing methods fail we fail with an rc of
|
||||
# 0. This logic is added to the end of the cmd at the bottom of this
|
||||
# function.
|
||||
|
||||
# Return codes:
|
||||
# checksum: success!
|
||||
# 0: Unknown error
|
||||
# 1: Remote file does not exist
|
||||
# 2: No read permissions on the file
|
||||
# 3: File is a directory
|
||||
# 4: No python interpreter
|
||||
|
||||
# Quoting gets complex here. We're writing a python string that's
|
||||
# used by a variety of shells on the remote host to invoke a python
|
||||
# "one-liner".
|
||||
shell_escaped_path = pipes.quote(path)
|
||||
test = "rc=flag; [ -r %(p)s ] || rc=2; [ -f %(p)s ] || rc=1; [ -d %(p)s ] && rc=3; %(i)s -V 2>/dev/null || rc=4; [ x\"$rc\" != \"xflag\" ] && echo \"${rc} \"%(p)s && exit 0" % dict(p=shell_escaped_path, i=python_interp)
|
||||
csums = [
|
||||
"({0} -c 'import hashlib; BLOCKSIZE = 65536; hasher = hashlib.sha1();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # Python > 2.4 (including python3)
|
||||
"({0} -c 'import sha; BLOCKSIZE = 65536; hasher = sha.sha();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # Python == 2.4
|
||||
]
|
||||
|
||||
cmd = " || ".join(csums)
|
||||
cmd = "%s; %s || (echo \'0 \'%s)" % (test, cmd, shell_escaped_path)
|
||||
return cmd
|
||||
|
||||
def build_module_command(self, env_string, shebang, cmd, rm_tmp=None):
|
||||
cmd_parts = [env_string.strip(), shebang.replace("#!", "").strip(), cmd]
|
||||
new_cmd = " ".join(cmd_parts)
|
||||
if rm_tmp:
|
||||
new_cmd = '%s; rm -rf %s >/dev/null 2>&1' % (new_cmd, rm_tmp)
|
||||
return new_cmd
|
||||
432
lib/ansible/plugins/strategies/__init__.py
Normal file
432
lib/ansible/plugins/strategies/__init__.py
Normal file
@@ -0,0 +1,432 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from six.moves import queue as Queue
|
||||
import time
|
||||
|
||||
from ansible.errors import *
|
||||
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
|
||||
from ansible.playbook.handler import Handler
|
||||
from ansible.playbook.helpers import load_list_of_blocks
|
||||
from ansible.playbook.role import ROLE_CACHE, hash_params
|
||||
from ansible.plugins import filter_loader, lookup_loader, module_loader
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
|
||||
__all__ = ['StrategyBase']
|
||||
|
||||
# FIXME: this should probably be in the plugins/__init__.py, with
|
||||
# a smarter mechanism to set all of the attributes based on
|
||||
# the loaders created there
|
||||
class SharedPluginLoaderObj:
|
||||
'''
|
||||
A simple object to make pass the various plugin loaders to
|
||||
the forked processes over the queue easier
|
||||
'''
|
||||
def __init__(self):
|
||||
self.filter_loader = filter_loader
|
||||
self.lookup_loader = lookup_loader
|
||||
self.module_loader = module_loader
|
||||
|
||||
class StrategyBase:
|
||||
|
||||
'''
|
||||
This is the base class for strategy plugins, which contains some common
|
||||
code useful to all strategies like running handlers, cleanup actions, etc.
|
||||
'''
|
||||
|
||||
def __init__(self, tqm):
|
||||
self._tqm = tqm
|
||||
self._inventory = tqm.get_inventory()
|
||||
self._workers = tqm.get_workers()
|
||||
self._notified_handlers = tqm.get_notified_handlers()
|
||||
#self._callback = tqm.get_callback()
|
||||
self._variable_manager = tqm.get_variable_manager()
|
||||
self._loader = tqm.get_loader()
|
||||
self._final_q = tqm._final_q
|
||||
|
||||
# internal counters
|
||||
self._pending_results = 0
|
||||
self._cur_worker = 0
|
||||
|
||||
# this dictionary is used to keep track of hosts that have
|
||||
# outstanding tasks still in queue
|
||||
self._blocked_hosts = dict()
|
||||
|
||||
def run(self, iterator, connection_info, result=True):
|
||||
# save the counts on failed/unreachable hosts, as the cleanup/handler
|
||||
# methods will clear that information during their runs
|
||||
num_failed = len(self._tqm._failed_hosts)
|
||||
num_unreachable = len(self._tqm._unreachable_hosts)
|
||||
|
||||
#debug("running the cleanup portion of the play")
|
||||
#result &= self.cleanup(iterator, connection_info)
|
||||
debug("running handlers")
|
||||
result &= self.run_handlers(iterator, connection_info)
|
||||
|
||||
# send the stats callback
|
||||
self._tqm.send_callback('v2_playbook_on_stats', self._tqm._stats)
|
||||
|
||||
if not result:
|
||||
if num_unreachable > 0:
|
||||
return 3
|
||||
elif num_failed > 0:
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_hosts_remaining(self, play):
|
||||
return [host for host in self._inventory.get_hosts(play.hosts) if host.name not in self._tqm._failed_hosts and host.name not in self._tqm._unreachable_hosts]
|
||||
|
||||
def get_failed_hosts(self, play):
|
||||
return [host for host in self._inventory.get_hosts(play.hosts) if host.name in self._tqm._failed_hosts]
|
||||
|
||||
def _queue_task(self, host, task, task_vars, connection_info):
|
||||
''' handles queueing the task up to be sent to a worker '''
|
||||
|
||||
debug("entering _queue_task() for %s/%s" % (host, task))
|
||||
|
||||
# and then queue the new task
|
||||
debug("%s - putting task (%s) in queue" % (host, task))
|
||||
try:
|
||||
debug("worker is %d (out of %d available)" % (self._cur_worker+1, len(self._workers)))
|
||||
|
||||
(worker_prc, main_q, rslt_q) = self._workers[self._cur_worker]
|
||||
self._cur_worker += 1
|
||||
if self._cur_worker >= len(self._workers):
|
||||
self._cur_worker = 0
|
||||
|
||||
self._pending_results += 1
|
||||
|
||||
# create a dummy object with plugin loaders set as an easier
|
||||
# way to share them with the forked processes
|
||||
shared_loader_obj = SharedPluginLoaderObj()
|
||||
|
||||
main_q.put((host, task, self._loader.get_basedir(), task_vars, connection_info, shared_loader_obj), block=False)
|
||||
except (EOFError, IOError, AssertionError) as e:
|
||||
# most likely an abort
|
||||
debug("got an error while queuing: %s" % e)
|
||||
return
|
||||
debug("exiting _queue_task() for %s/%s" % (host, task))
|
||||
|
||||
def _process_pending_results(self, iterator):
|
||||
'''
|
||||
Reads results off the final queue and takes appropriate action
|
||||
based on the result (executing callbacks, updating state, etc.).
|
||||
'''
|
||||
|
||||
ret_results = []
|
||||
|
||||
while not self._final_q.empty() and not self._tqm._terminated:
|
||||
try:
|
||||
result = self._final_q.get(block=False)
|
||||
debug("got result from result worker: %s" % (result,))
|
||||
|
||||
# all host status messages contain 2 entries: (msg, task_result)
|
||||
if result[0] in ('host_task_ok', 'host_task_failed', 'host_task_skipped', 'host_unreachable'):
|
||||
task_result = result[1]
|
||||
host = task_result._host
|
||||
task = task_result._task
|
||||
if result[0] == 'host_task_failed':
|
||||
if not task.ignore_errors:
|
||||
debug("marking %s as failed" % host.name)
|
||||
iterator.mark_host_failed(host)
|
||||
self._tqm._failed_hosts[host.name] = True
|
||||
self._tqm._stats.increment('failures', host.name)
|
||||
self._tqm.send_callback('v2_runner_on_failed', task_result)
|
||||
elif result[0] == 'host_unreachable':
|
||||
self._tqm._unreachable_hosts[host.name] = True
|
||||
self._tqm._stats.increment('dark', host.name)
|
||||
self._tqm.send_callback('v2_runner_on_unreachable', task_result)
|
||||
elif result[0] == 'host_task_skipped':
|
||||
self._tqm._stats.increment('skipped', host.name)
|
||||
self._tqm.send_callback('v2_runner_on_skipped', task_result)
|
||||
elif result[0] == 'host_task_ok':
|
||||
self._tqm._stats.increment('ok', host.name)
|
||||
if 'changed' in task_result._result and task_result._result['changed']:
|
||||
self._tqm._stats.increment('changed', host.name)
|
||||
self._tqm.send_callback('v2_runner_on_ok', task_result)
|
||||
|
||||
self._pending_results -= 1
|
||||
if host.name in self._blocked_hosts:
|
||||
del self._blocked_hosts[host.name]
|
||||
|
||||
# If this is a role task, mark the parent role as being run (if
|
||||
# the task was ok or failed, but not skipped or unreachable)
|
||||
if task_result._task._role is not None and result[0] in ('host_task_ok', 'host_task_failed'):
|
||||
# lookup the role in the ROLE_CACHE to make sure we're dealing
|
||||
# with the correct object and mark it as executed
|
||||
for (entry, role_obj) in ROLE_CACHE[task_result._task._role._role_name].iteritems():
|
||||
hashed_entry = hash_params(task_result._task._role._role_params)
|
||||
if entry == hashed_entry :
|
||||
role_obj._had_task_run = True
|
||||
|
||||
ret_results.append(task_result)
|
||||
|
||||
elif result[0] == 'add_host':
|
||||
task_result = result[1]
|
||||
new_host_info = task_result.get('add_host', dict())
|
||||
|
||||
self._add_host(new_host_info)
|
||||
|
||||
elif result[0] == 'add_group':
|
||||
host = result[1]
|
||||
task_result = result[2]
|
||||
group_name = task_result.get('add_group')
|
||||
|
||||
self._add_group(host, group_name)
|
||||
|
||||
elif result[0] == 'notify_handler':
|
||||
host = result[1]
|
||||
handler_name = result[2]
|
||||
|
||||
if handler_name not in self._notified_handlers:
|
||||
self._notified_handlers[handler_name] = []
|
||||
|
||||
if host not in self._notified_handlers[handler_name]:
|
||||
self._notified_handlers[handler_name].append(host)
|
||||
|
||||
elif result[0] == 'set_host_var':
|
||||
host = result[1]
|
||||
var_name = result[2]
|
||||
var_value = result[3]
|
||||
self._variable_manager.set_host_variable(host, var_name, var_value)
|
||||
|
||||
elif result[0] == 'set_host_facts':
|
||||
host = result[1]
|
||||
facts = result[2]
|
||||
self._variable_manager.set_host_facts(host, facts)
|
||||
|
||||
else:
|
||||
raise AnsibleError("unknown result message received: %s" % result[0])
|
||||
except Queue.Empty:
|
||||
pass
|
||||
|
||||
return ret_results
|
||||
|
||||
def _wait_on_pending_results(self, iterator):
|
||||
'''
|
||||
Wait for the shared counter to drop to zero, using a short sleep
|
||||
between checks to ensure we don't spin lock
|
||||
'''
|
||||
|
||||
ret_results = []
|
||||
|
||||
while self._pending_results > 0 and not self._tqm._terminated:
|
||||
debug("waiting for pending results (%d left)" % self._pending_results)
|
||||
results = self._process_pending_results(iterator)
|
||||
ret_results.extend(results)
|
||||
if self._tqm._terminated:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
return ret_results
|
||||
|
||||
def _add_host(self, host_info):
|
||||
'''
|
||||
Helper function to add a new host to inventory based on a task result.
|
||||
'''
|
||||
|
||||
host_name = host_info.get('host_name')
|
||||
|
||||
# Check if host in cache, add if not
|
||||
if host_name in self._inventory._hosts_cache:
|
||||
new_host = self._inventory._hosts_cache[host_name]
|
||||
else:
|
||||
new_host = Host(host_name)
|
||||
self._inventory._hosts_cache[host_name] = new_host
|
||||
|
||||
allgroup = self._inventory.get_group('all')
|
||||
allgroup.add_host(new_host)
|
||||
|
||||
# Set/update the vars for this host
|
||||
# FIXME: probably should have a set vars method for the host?
|
||||
new_vars = host_info.get('host_vars', dict())
|
||||
new_host.vars.update(new_vars)
|
||||
|
||||
new_groups = host_info.get('groups', [])
|
||||
for group_name in new_groups:
|
||||
if not self._inventory.get_group(group_name):
|
||||
new_group = Group(group_name)
|
||||
self._inventory.add_group(new_group)
|
||||
new_group.vars = self._inventory.get_group_variables(group_name)
|
||||
else:
|
||||
new_group = self._inventory.get_group(group_name)
|
||||
|
||||
new_group.add_host(new_host)
|
||||
|
||||
# add this host to the group cache
|
||||
if self._inventory._groups_list is not None:
|
||||
if group_name in self._inventory._groups_list:
|
||||
if new_host.name not in self._inventory._groups_list[group_name]:
|
||||
self._inventory._groups_list[group_name].append(new_host.name)
|
||||
|
||||
# clear pattern caching completely since it's unpredictable what
|
||||
# patterns may have referenced the group
|
||||
# FIXME: is this still required?
|
||||
self._inventory.clear_pattern_cache()
|
||||
|
||||
def _add_group(self, host, group_name):
|
||||
'''
|
||||
Helper function to add a group (if it does not exist), and to assign the
|
||||
specified host to that group.
|
||||
'''
|
||||
|
||||
new_group = self._inventory.get_group(group_name)
|
||||
if not new_group:
|
||||
# create the new group and add it to inventory
|
||||
new_group = Group(group_name)
|
||||
self._inventory.add_group(new_group)
|
||||
|
||||
# and add the group to the proper hierarchy
|
||||
allgroup = self._inventory.get_group('all')
|
||||
allgroup.add_child_group(new_group)
|
||||
|
||||
# the host here is from the executor side, which means it was a
|
||||
# serialized/cloned copy and we'll need to look up the proper
|
||||
# host object from the master inventory
|
||||
actual_host = self._inventory.get_host(host.name)
|
||||
|
||||
# and add the host to the group
|
||||
new_group.add_host(actual_host)
|
||||
|
||||
def _load_included_file(self, included_file):
|
||||
'''
|
||||
Loads an included YAML file of tasks, applying the optional set of variables.
|
||||
'''
|
||||
|
||||
data = self._loader.load_from_file(included_file._filename)
|
||||
if not isinstance(data, list):
|
||||
raise AnsibleParserError("included task files must contain a list of tasks", obj=included_file._task._ds)
|
||||
|
||||
is_handler = isinstance(included_file._task, Handler)
|
||||
block_list = load_list_of_blocks(
|
||||
data,
|
||||
play=included_file._task._block._play,
|
||||
parent_block=included_file._task._block,
|
||||
task_include=included_file._task,
|
||||
role=included_file._task._role,
|
||||
use_handlers=is_handler,
|
||||
loader=self._loader
|
||||
)
|
||||
|
||||
# set the vars for this task from those specified as params to the include
|
||||
for b in block_list:
|
||||
b._vars = included_file._args.copy()
|
||||
|
||||
return block_list
|
||||
|
||||
def cleanup(self, iterator, connection_info):
|
||||
'''
|
||||
Iterates through failed hosts and runs any outstanding rescue/always blocks
|
||||
and handlers which may still need to be run after a failure.
|
||||
'''
|
||||
|
||||
debug("in cleanup")
|
||||
result = True
|
||||
|
||||
debug("getting failed hosts")
|
||||
failed_hosts = self.get_failed_hosts(iterator._play)
|
||||
if len(failed_hosts) == 0:
|
||||
debug("there are no failed hosts")
|
||||
return result
|
||||
|
||||
debug("marking hosts failed in the iterator")
|
||||
# mark the host as failed in the iterator so it will take
|
||||
# any required rescue paths which may be outstanding
|
||||
for host in failed_hosts:
|
||||
iterator.mark_host_failed(host)
|
||||
|
||||
debug("clearing the failed hosts list")
|
||||
# clear the failed hosts dictionary now while also
|
||||
for entry in self._tqm._failed_hosts.keys():
|
||||
del self._tqm._failed_hosts[entry]
|
||||
|
||||
work_to_do = True
|
||||
while work_to_do:
|
||||
work_to_do = False
|
||||
for host in failed_hosts:
|
||||
host_name = host.name
|
||||
|
||||
if host_name in self._tqm._failed_hosts:
|
||||
iterator.mark_host_failed(host)
|
||||
del self._tqm._failed_hosts[host_name]
|
||||
|
||||
if host_name in self._blocked_hosts:
|
||||
work_to_do = True
|
||||
continue
|
||||
elif iterator.get_next_task_for_host(host, peek=True) and host_name not in self._tqm._unreachable_hosts:
|
||||
work_to_do = True
|
||||
|
||||
# pop the task, mark the host blocked, and queue it
|
||||
self._blocked_hosts[host_name] = True
|
||||
task = iterator.get_next_task_for_host(host)
|
||||
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=task)
|
||||
self._tqm.send_callback('v2_playbook_on_cleanup_task_start', task)
|
||||
self._queue_task(host, task, task_vars, connection_info)
|
||||
|
||||
self._process_pending_results(iterator)
|
||||
time.sleep(0.01)
|
||||
|
||||
# no more work, wait until the queue is drained
|
||||
self._wait_on_pending_results(iterator)
|
||||
|
||||
return result
|
||||
|
||||
def run_handlers(self, iterator, connection_info):
|
||||
'''
|
||||
Runs handlers on those hosts which have been notified.
|
||||
'''
|
||||
|
||||
result = True
|
||||
|
||||
# FIXME: getting the handlers from the iterators play should be
|
||||
# a method on the iterator, which may also filter the list
|
||||
# of handlers based on the notified list
|
||||
|
||||
for handler_block in iterator._play.handlers:
|
||||
# FIXME: handlers need to support the rescue/always portions of blocks too,
|
||||
# but this may take some work in the iterator and gets tricky when
|
||||
# we consider the ability of meta tasks to flush handlers
|
||||
for handler in handler_block.block:
|
||||
handler_name = handler.get_name()
|
||||
if handler_name in self._notified_handlers and len(self._notified_handlers[handler_name]):
|
||||
if not len(self.get_hosts_remaining(iterator._play)):
|
||||
self._tqm.send_callback('v2_playbook_on_no_hosts_remaining')
|
||||
result = False
|
||||
break
|
||||
self._tqm.send_callback('v2_playbook_on_handler_task_start', handler)
|
||||
for host in self._notified_handlers[handler_name]:
|
||||
if not handler.has_triggered(host):
|
||||
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=handler)
|
||||
self._queue_task(host, handler, task_vars, connection_info)
|
||||
handler.flag_for_host(host)
|
||||
self._process_pending_results(iterator)
|
||||
self._wait_on_pending_results(iterator)
|
||||
# wipe the notification list
|
||||
self._notified_handlers[handler_name] = []
|
||||
debug("done running handlers, result is: %s" % result)
|
||||
return result
|
||||
151
lib/ansible/plugins/strategies/free.py
Normal file
151
lib/ansible/plugins/strategies/free.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import time
|
||||
|
||||
from ansible.plugins.strategies import StrategyBase
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
class StrategyModule(StrategyBase):
|
||||
|
||||
def run(self, iterator, connection_info):
|
||||
'''
|
||||
The "free" strategy is a bit more complex, in that it allows tasks to
|
||||
be sent to hosts as quickly as they can be processed. This means that
|
||||
some hosts may finish very quickly if run tasks result in little or no
|
||||
work being done versus other systems.
|
||||
|
||||
The algorithm used here also tries to be more "fair" when iterating
|
||||
through hosts by remembering the last host in the list to be given a task
|
||||
and starting the search from there as opposed to the top of the hosts
|
||||
list again, which would end up favoring hosts near the beginning of the
|
||||
list.
|
||||
'''
|
||||
|
||||
# the last host to be given a task
|
||||
last_host = 0
|
||||
|
||||
result = True
|
||||
|
||||
work_to_do = True
|
||||
while work_to_do and not self._tqm._terminated:
|
||||
|
||||
hosts_left = self.get_hosts_remaining(iterator._play)
|
||||
if len(hosts_left) == 0:
|
||||
self._tqm.send_callback('v2_playbook_on_no_hosts_remaining')
|
||||
result = False
|
||||
break
|
||||
|
||||
work_to_do = False # assume we have no more work to do
|
||||
starting_host = last_host # save current position so we know when we've
|
||||
# looped back around and need to break
|
||||
|
||||
# try and find an unblocked host with a task to run
|
||||
host_results = []
|
||||
while True:
|
||||
host = hosts_left[last_host]
|
||||
debug("next free host: %s" % host)
|
||||
host_name = host.get_name()
|
||||
|
||||
# peek at the next task for the host, to see if there's
|
||||
# anything to do do for this host
|
||||
(state, task) = iterator.get_next_task_for_host(host, peek=True)
|
||||
debug("free host state: %s" % state)
|
||||
debug("free host task: %s" % task)
|
||||
if host_name not in self._tqm._failed_hosts and host_name not in self._tqm._unreachable_hosts and task:
|
||||
|
||||
# set the flag so the outer loop knows we've still found
|
||||
# some work which needs to be done
|
||||
work_to_do = True
|
||||
|
||||
debug("this host has work to do")
|
||||
|
||||
# check to see if this host is blocked (still executing a previous task)
|
||||
if not host_name in self._blocked_hosts:
|
||||
# pop the task, mark the host blocked, and queue it
|
||||
self._blocked_hosts[host_name] = True
|
||||
(state, task) = iterator.get_next_task_for_host(host)
|
||||
|
||||
debug("getting variables")
|
||||
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=task)
|
||||
debug("done getting variables")
|
||||
|
||||
# check to see if this task should be skipped, due to it being a member of a
|
||||
# role which has already run (and whether that role allows duplicate execution)
|
||||
if task._role and task._role.has_run():
|
||||
# If there is no metadata, the default behavior is to not allow duplicates,
|
||||
# if there is metadata, check to see if the allow_duplicates flag was set to true
|
||||
if task._role._metadata is None or task._role._metadata and not task._role._metadata.allow_duplicates:
|
||||
debug("'%s' skipped because role has already run" % task)
|
||||
continue
|
||||
|
||||
if not task.evaluate_tags(connection_info.only_tags, connection_info.skip_tags, task_vars) and task.action != 'setup':
|
||||
debug("'%s' failed tag evaluation" % task)
|
||||
continue
|
||||
|
||||
if task.action == 'meta':
|
||||
# meta tasks store their args in the _raw_params field of args,
|
||||
# since they do not use k=v pairs, so get that
|
||||
meta_action = task.args.get('_raw_params')
|
||||
if meta_action == 'noop':
|
||||
# FIXME: issue a callback for the noop here?
|
||||
continue
|
||||
elif meta_action == 'flush_handlers':
|
||||
# FIXME: in the 'free' mode, flushing handlers should result in
|
||||
# only those handlers notified for the host doing the flush
|
||||
self.run_handlers(iterator, connection_info)
|
||||
else:
|
||||
raise AnsibleError("invalid meta action requested: %s" % meta_action, obj=task._ds)
|
||||
|
||||
self._blocked_hosts[host_name] = False
|
||||
else:
|
||||
self._tqm.send_callback('v2_playbook_on_task_start', task, is_conditional=False)
|
||||
self._queue_task(host, task, task_vars, connection_info)
|
||||
|
||||
# move on to the next host and make sure we
|
||||
# haven't gone past the end of our hosts list
|
||||
last_host += 1
|
||||
if last_host > len(hosts_left) - 1:
|
||||
last_host = 0
|
||||
|
||||
# if we've looped around back to the start, break out
|
||||
if last_host == starting_host:
|
||||
break
|
||||
|
||||
results = self._process_pending_results(iterator)
|
||||
host_results.extend(results)
|
||||
|
||||
# pause briefly so we don't spin lock
|
||||
time.sleep(0.05)
|
||||
|
||||
try:
|
||||
results = self._wait_on_pending_results(iterator)
|
||||
host_results.extend(results)
|
||||
except Exception as e:
|
||||
# FIXME: ctrl+c can cause some failures here, so catch them
|
||||
# with the appropriate error type
|
||||
print("wtf: %s" % e)
|
||||
pass
|
||||
|
||||
# run the base class run() method, which executes the cleanup function
|
||||
# and runs any outstanding handlers which have been triggered
|
||||
super(StrategyModule, self).run(iterator, connection_info)
|
||||
|
||||
307
lib/ansible/plugins/strategies/linear.py
Normal file
307
lib/ansible/plugins/strategies/linear.py
Normal file
@@ -0,0 +1,307 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.executor.play_iterator import PlayIterator
|
||||
from ansible.playbook.block import Block
|
||||
from ansible.playbook.task import Task
|
||||
from ansible.plugins import action_loader
|
||||
from ansible.plugins.strategies import StrategyBase
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
class StrategyModule(StrategyBase):
|
||||
|
||||
def _get_next_task_lockstep(self, hosts, iterator):
|
||||
'''
|
||||
Returns a list of (host, task) tuples, where the task may
|
||||
be a noop task to keep the iterator in lock step across
|
||||
all hosts.
|
||||
'''
|
||||
|
||||
noop_task = Task()
|
||||
noop_task.action = 'meta'
|
||||
noop_task.args['_raw_params'] = 'noop'
|
||||
noop_task.set_loader(iterator._play._loader)
|
||||
|
||||
host_tasks = {}
|
||||
for host in hosts:
|
||||
host_tasks[host.name] = iterator.get_next_task_for_host(host, peek=True)
|
||||
|
||||
num_setups = 0
|
||||
num_tasks = 0
|
||||
num_rescue = 0
|
||||
num_always = 0
|
||||
|
||||
lowest_cur_block = len(iterator._blocks)
|
||||
|
||||
for (k, v) in host_tasks.iteritems():
|
||||
if v is None:
|
||||
continue
|
||||
|
||||
(s, t) = v
|
||||
if s.cur_block < lowest_cur_block and s.run_state != PlayIterator.ITERATING_COMPLETE:
|
||||
lowest_cur_block = s.cur_block
|
||||
|
||||
if s.run_state == PlayIterator.ITERATING_SETUP:
|
||||
num_setups += 1
|
||||
elif s.run_state == PlayIterator.ITERATING_TASKS:
|
||||
num_tasks += 1
|
||||
elif s.run_state == PlayIterator.ITERATING_RESCUE:
|
||||
num_rescue += 1
|
||||
elif s.run_state == PlayIterator.ITERATING_ALWAYS:
|
||||
num_always += 1
|
||||
|
||||
def _advance_selected_hosts(hosts, cur_block, cur_state):
|
||||
'''
|
||||
This helper returns the task for all hosts in the requested
|
||||
state, otherwise they get a noop dummy task. This also advances
|
||||
the state of the host, since the given states are determined
|
||||
while using peek=True.
|
||||
'''
|
||||
# we return the values in the order they were originally
|
||||
# specified in the given hosts array
|
||||
rvals = []
|
||||
for host in hosts:
|
||||
(s, t) = host_tasks[host.name]
|
||||
if s.run_state == cur_state and s.cur_block == cur_block:
|
||||
new_t = iterator.get_next_task_for_host(host)
|
||||
#if new_t != t:
|
||||
# raise AnsibleError("iterator error, wtf?")
|
||||
rvals.append((host, t))
|
||||
else:
|
||||
rvals.append((host, noop_task))
|
||||
return rvals
|
||||
|
||||
# if any hosts are in ITERATING_SETUP, return the setup task
|
||||
# while all other hosts get a noop
|
||||
if num_setups:
|
||||
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_SETUP)
|
||||
|
||||
# if any hosts are in ITERATING_TASKS, return the next normal
|
||||
# task for these hosts, while all other hosts get a noop
|
||||
if num_tasks:
|
||||
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_TASKS)
|
||||
|
||||
# if any hosts are in ITERATING_RESCUE, return the next rescue
|
||||
# task for these hosts, while all other hosts get a noop
|
||||
if num_rescue:
|
||||
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_RESCUE)
|
||||
|
||||
# if any hosts are in ITERATING_ALWAYS, return the next always
|
||||
# task for these hosts, while all other hosts get a noop
|
||||
if num_always:
|
||||
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_ALWAYS)
|
||||
|
||||
# at this point, everything must be ITERATING_COMPLETE, so we
|
||||
# return None for all hosts in the list
|
||||
return [(host, None) for host in hosts]
|
||||
|
||||
|
||||
def run(self, iterator, connection_info):
|
||||
'''
|
||||
The linear strategy is simple - get the next task and queue
|
||||
it for all hosts, then wait for the queue to drain before
|
||||
moving on to the next task
|
||||
'''
|
||||
|
||||
result = True
|
||||
|
||||
# iteratate over each task, while there is one left to run
|
||||
work_to_do = True
|
||||
while work_to_do and not self._tqm._terminated:
|
||||
|
||||
try:
|
||||
debug("getting the remaining hosts for this loop")
|
||||
self._tqm._failed_hosts = iterator.get_failed_hosts()
|
||||
hosts_left = self.get_hosts_remaining(iterator._play)
|
||||
debug("done getting the remaining hosts for this loop")
|
||||
if len(hosts_left) == 0:
|
||||
debug("out of hosts to run on")
|
||||
self._tqm.send_callback('v2_playbook_on_no_hosts_remaining')
|
||||
result = False
|
||||
break
|
||||
|
||||
# queue up this task for each host in the inventory
|
||||
callback_sent = False
|
||||
work_to_do = False
|
||||
|
||||
host_results = []
|
||||
host_tasks = self._get_next_task_lockstep(hosts_left, iterator)
|
||||
for (host, task) in host_tasks:
|
||||
if not task:
|
||||
continue
|
||||
|
||||
run_once = False
|
||||
work_to_do = True
|
||||
|
||||
# test to see if the task across all hosts points to an action plugin which
|
||||
# sets BYPASS_HOST_LOOP to true, or if it has run_once enabled. If so, we
|
||||
# will only send this task to the first host in the list.
|
||||
|
||||
try:
|
||||
action = action_loader.get(task.action, class_only=True)
|
||||
if task.run_once or getattr(action, 'BYPASS_HOST_LOOP', False):
|
||||
run_once = True
|
||||
except KeyError:
|
||||
# we don't care here, because the action may simply not have a
|
||||
# corresponding action plugin
|
||||
pass
|
||||
|
||||
debug("getting variables")
|
||||
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=task)
|
||||
debug("done getting variables")
|
||||
|
||||
# check to see if this task should be skipped, due to it being a member of a
|
||||
# role which has already run (and whether that role allows duplicate execution)
|
||||
if task._role and task._role.has_run():
|
||||
# If there is no metadata, the default behavior is to not allow duplicates,
|
||||
# if there is metadata, check to see if the allow_duplicates flag was set to true
|
||||
if task._role._metadata is None or task._role._metadata and not task._role._metadata.allow_duplicates:
|
||||
debug("'%s' skipped because role has already run" % task)
|
||||
continue
|
||||
|
||||
if task.action == 'meta':
|
||||
# meta tasks store their args in the _raw_params field of args,
|
||||
# since they do not use k=v pairs, so get that
|
||||
meta_action = task.args.get('_raw_params')
|
||||
if meta_action == 'noop':
|
||||
# FIXME: issue a callback for the noop here?
|
||||
continue
|
||||
elif meta_action == 'flush_handlers':
|
||||
self.run_handlers(iterator, connection_info)
|
||||
else:
|
||||
raise AnsibleError("invalid meta action requested: %s" % meta_action, obj=task._ds)
|
||||
else:
|
||||
if not callback_sent:
|
||||
self._tqm.send_callback('v2_playbook_on_task_start', task, is_conditional=False)
|
||||
callback_sent = True
|
||||
|
||||
self._blocked_hosts[host.get_name()] = True
|
||||
self._queue_task(host, task, task_vars, connection_info)
|
||||
|
||||
results = self._process_pending_results(iterator)
|
||||
host_results.extend(results)
|
||||
|
||||
# if we're bypassing the host loop, break out now
|
||||
if run_once:
|
||||
break
|
||||
|
||||
debug("done queuing things up, now waiting for results queue to drain")
|
||||
results = self._wait_on_pending_results(iterator)
|
||||
host_results.extend(results)
|
||||
|
||||
# FIXME: this needs to be somewhere else
|
||||
class IncludedFile:
|
||||
def __init__(self, filename, args, task):
|
||||
self._filename = filename
|
||||
self._args = args
|
||||
self._task = task
|
||||
self._hosts = []
|
||||
def add_host(self, host):
|
||||
if host not in self._hosts:
|
||||
self._hosts.append(host)
|
||||
def __eq__(self, other):
|
||||
return other._filename == self._filename and other._args == self._args
|
||||
def __repr__(self):
|
||||
return "%s (%s): %s" % (self._filename, self._args, self._hosts)
|
||||
|
||||
# FIXME: this should also be moved to the base class in a method
|
||||
included_files = []
|
||||
for res in host_results:
|
||||
if res._task.action == 'include':
|
||||
if res._task.loop:
|
||||
include_results = res._result['results']
|
||||
else:
|
||||
include_results = [ res._result ]
|
||||
|
||||
for include_result in include_results:
|
||||
# if the task result was skipped or failed, continue
|
||||
if 'skipped' in include_result and include_result['skipped'] or 'failed' in include_result:
|
||||
continue
|
||||
|
||||
original_task = iterator.get_original_task(res._host, res._task)
|
||||
if original_task and original_task._role:
|
||||
include_file = self._loader.path_dwim_relative(original_task._role._role_path, 'tasks', include_result['include'])
|
||||
else:
|
||||
include_file = self._loader.path_dwim(res._task.args.get('_raw_params'))
|
||||
|
||||
include_variables = include_result.get('include_variables', dict())
|
||||
if 'item' in include_result:
|
||||
include_variables['item'] = include_result['item']
|
||||
|
||||
inc_file = IncludedFile(include_file, include_variables, original_task)
|
||||
|
||||
try:
|
||||
pos = included_files.index(inc_file)
|
||||
inc_file = included_files[pos]
|
||||
except ValueError:
|
||||
included_files.append(inc_file)
|
||||
|
||||
inc_file.add_host(res._host)
|
||||
|
||||
# FIXME: should this be moved into the iterator class? Main downside would be
|
||||
# that accessing the TQM's callback member would be more difficult, if
|
||||
# we do want to send callbacks from here
|
||||
if len(included_files) > 0:
|
||||
noop_task = Task()
|
||||
noop_task.action = 'meta'
|
||||
noop_task.args['_raw_params'] = 'noop'
|
||||
noop_task.set_loader(iterator._play._loader)
|
||||
|
||||
all_blocks = dict((host, []) for host in hosts_left)
|
||||
for included_file in included_files:
|
||||
# included hosts get the task list while those excluded get an equal-length
|
||||
# list of noop tasks, to make sure that they continue running in lock-step
|
||||
try:
|
||||
new_blocks = self._load_included_file(included_file)
|
||||
except AnsibleError, e:
|
||||
for host in included_file._hosts:
|
||||
iterator.mark_host_failed(host)
|
||||
# FIXME: callback here?
|
||||
print(e)
|
||||
|
||||
for new_block in new_blocks:
|
||||
noop_block = Block(parent_block=task._block)
|
||||
noop_block.block = [noop_task for t in new_block.block]
|
||||
noop_block.always = [noop_task for t in new_block.always]
|
||||
noop_block.rescue = [noop_task for t in new_block.rescue]
|
||||
for host in hosts_left:
|
||||
if host in included_file._hosts:
|
||||
task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, host=host, task=included_file._task)
|
||||
final_block = new_block.filter_tagged_tasks(connection_info, task_vars)
|
||||
all_blocks[host].append(final_block)
|
||||
else:
|
||||
all_blocks[host].append(noop_block)
|
||||
|
||||
for host in hosts_left:
|
||||
iterator.add_tasks(host, all_blocks[host])
|
||||
|
||||
debug("results queue empty")
|
||||
except (IOError, EOFError), e:
|
||||
debug("got IOError/EOFError in task loop: %s" % e)
|
||||
# most likely an abort, return failed
|
||||
return 1
|
||||
|
||||
# run the base class run() method, which executes the cleanup function
|
||||
# and runs any outstanding handlers which have been triggered
|
||||
|
||||
return super(StrategyModule, self).run(iterator, connection_info, result)
|
||||
|
||||
21
lib/ansible/plugins/vars/__init__.py
Normal file
21
lib/ansible/plugins/vars/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# (c) 2012-2014, 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/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
Reference in New Issue
Block a user