mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-07 05:42:50 +00:00
Merge branch 'v2_final' into devel_switch_v2
Conflicts: lib/ansible/inventory/__init__.py lib/ansible/modules/core lib/ansible/utils/__init__.py lib/ansible/utils/module_docs.py
This commit is contained in:
@@ -15,4 +15,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
__version__ = '2.0.0'
|
||||
__author__ = 'Michael DeHaan'
|
||||
__author__ = 'Ansible, Inc.'
|
||||
|
||||
143
lib/ansible/cache/jsonfile.py
vendored
143
lib/ansible/cache/jsonfile.py
vendored
@@ -1,143 +0,0 @@
|
||||
# (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/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
import errno
|
||||
import codecs
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible import utils
|
||||
from ansible.cache.base import BaseCacheModule
|
||||
|
||||
class CacheModule(BaseCacheModule):
|
||||
"""
|
||||
A caching module backed by json files.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self._timeout = float(C.CACHE_PLUGIN_TIMEOUT)
|
||||
self._cache = {}
|
||||
self._cache_dir = C.CACHE_PLUGIN_CONNECTION # expects a dir path
|
||||
if not self._cache_dir:
|
||||
utils.exit("error, fact_caching_connection is not set, cannot use fact cache")
|
||||
|
||||
if not os.path.exists(self._cache_dir):
|
||||
try:
|
||||
os.makedirs(self._cache_dir)
|
||||
except (OSError,IOError), e:
|
||||
utils.warning("error while trying to create cache dir %s : %s" % (self._cache_dir, str(e)))
|
||||
return None
|
||||
|
||||
def get(self, key):
|
||||
|
||||
if key in self._cache:
|
||||
return self._cache.get(key)
|
||||
|
||||
if self.has_expired(key):
|
||||
raise KeyError
|
||||
|
||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
||||
try:
|
||||
f = codecs.open(cachefile, 'r', encoding='utf-8')
|
||||
except (OSError,IOError), e:
|
||||
utils.warning("error while trying to read %s : %s" % (cachefile, str(e)))
|
||||
else:
|
||||
value = json.load(f)
|
||||
self._cache[key] = value
|
||||
return value
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def set(self, key, value):
|
||||
|
||||
self._cache[key] = value
|
||||
|
||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
||||
try:
|
||||
f = codecs.open(cachefile, 'w', encoding='utf-8')
|
||||
except (OSError,IOError), e:
|
||||
utils.warning("error while trying to write to %s : %s" % (cachefile, str(e)))
|
||||
else:
|
||||
f.write(utils.jsonify(value))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def has_expired(self, key):
|
||||
|
||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
||||
try:
|
||||
st = os.stat(cachefile)
|
||||
except (OSError,IOError), e:
|
||||
if e.errno == errno.ENOENT:
|
||||
return False
|
||||
else:
|
||||
utils.warning("error while trying to stat %s : %s" % (cachefile, str(e)))
|
||||
|
||||
if time.time() - st.st_mtime <= self._timeout:
|
||||
return False
|
||||
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
return True
|
||||
|
||||
def keys(self):
|
||||
keys = []
|
||||
for k in os.listdir(self._cache_dir):
|
||||
if not (k.startswith('.') or self.has_expired(k)):
|
||||
keys.append(k)
|
||||
return keys
|
||||
|
||||
def contains(self, key):
|
||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
||||
|
||||
if key in self._cache:
|
||||
return True
|
||||
|
||||
if self.has_expired(key):
|
||||
return False
|
||||
try:
|
||||
st = os.stat(cachefile)
|
||||
return True
|
||||
except (OSError,IOError), e:
|
||||
if e.errno == errno.ENOENT:
|
||||
return False
|
||||
else:
|
||||
utils.warning("error while trying to stat %s : %s" % (cachefile, str(e)))
|
||||
|
||||
def delete(self, key):
|
||||
del self._cache[key]
|
||||
try:
|
||||
os.remove("%s/%s" % (self._cache_dir, key))
|
||||
except (OSError,IOError), e:
|
||||
pass #TODO: only pass on non existing?
|
||||
|
||||
def flush(self):
|
||||
self._cache = {}
|
||||
for key in self.keys():
|
||||
self.delete(key)
|
||||
|
||||
def copy(self):
|
||||
ret = dict()
|
||||
for key in self.keys():
|
||||
ret[key] = self.get(key)
|
||||
return ret
|
||||
@@ -1,729 +0,0 @@
|
||||
# (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/>.
|
||||
|
||||
import utils
|
||||
import sys
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
import random
|
||||
import fnmatch
|
||||
import tempfile
|
||||
import fcntl
|
||||
import constants
|
||||
import locale
|
||||
from ansible.color import stringc
|
||||
from ansible.module_utils import basic
|
||||
from ansible.utils.unicode import to_unicode, to_bytes
|
||||
|
||||
import logging
|
||||
if constants.DEFAULT_LOG_PATH != '':
|
||||
path = constants.DEFAULT_LOG_PATH
|
||||
|
||||
if (os.path.exists(path) and not os.access(path, os.W_OK)) and not os.access(os.path.dirname(path), os.W_OK):
|
||||
sys.stderr.write("log file at %s is not writeable, aborting\n" % path)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
logging.basicConfig(filename=path, level=logging.DEBUG, format='%(asctime)s %(name)s %(message)s')
|
||||
mypid = str(os.getpid())
|
||||
user = getpass.getuser()
|
||||
logger = logging.getLogger("p=%s u=%s | " % (mypid, user))
|
||||
|
||||
callback_plugins = []
|
||||
|
||||
def load_callback_plugins():
|
||||
global callback_plugins
|
||||
callback_plugins = [x for x in utils.plugins.callback_loader.all()]
|
||||
|
||||
def get_cowsay_info():
|
||||
if constants.ANSIBLE_NOCOWS:
|
||||
return (None, None)
|
||||
cowsay = None
|
||||
if os.path.exists("/usr/bin/cowsay"):
|
||||
cowsay = "/usr/bin/cowsay"
|
||||
elif os.path.exists("/usr/games/cowsay"):
|
||||
cowsay = "/usr/games/cowsay"
|
||||
elif os.path.exists("/usr/local/bin/cowsay"):
|
||||
# BSD path for cowsay
|
||||
cowsay = "/usr/local/bin/cowsay"
|
||||
elif os.path.exists("/opt/local/bin/cowsay"):
|
||||
# MacPorts path for cowsay
|
||||
cowsay = "/opt/local/bin/cowsay"
|
||||
|
||||
noncow = os.getenv("ANSIBLE_COW_SELECTION",None)
|
||||
if cowsay and noncow == 'random':
|
||||
cmd = subprocess.Popen([cowsay, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
cows = out.split()
|
||||
cows.append(False)
|
||||
noncow = random.choice(cows)
|
||||
return (cowsay, noncow)
|
||||
|
||||
cowsay, noncow = get_cowsay_info()
|
||||
|
||||
def log_lockfile():
|
||||
# create the path for the lockfile and open it
|
||||
tempdir = tempfile.gettempdir()
|
||||
uid = os.getuid()
|
||||
path = os.path.join(tempdir, ".ansible-lock.%s" % uid)
|
||||
lockfile = open(path, 'w')
|
||||
# use fcntl to set FD_CLOEXEC on the file descriptor,
|
||||
# so that we don't leak the file descriptor later
|
||||
lockfile_fd = lockfile.fileno()
|
||||
old_flags = fcntl.fcntl(lockfile_fd, fcntl.F_GETFD)
|
||||
fcntl.fcntl(lockfile_fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
|
||||
return lockfile
|
||||
|
||||
LOG_LOCK = log_lockfile()
|
||||
|
||||
def log_flock(runner):
|
||||
if runner is not None:
|
||||
try:
|
||||
fcntl.lockf(runner.output_lockfile, fcntl.LOCK_EX)
|
||||
except OSError:
|
||||
# already got closed?
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
fcntl.lockf(LOG_LOCK, fcntl.LOCK_EX)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def log_unflock(runner):
|
||||
if runner is not None:
|
||||
try:
|
||||
fcntl.lockf(runner.output_lockfile, fcntl.LOCK_UN)
|
||||
except OSError:
|
||||
# already got closed?
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
fcntl.lockf(LOG_LOCK, fcntl.LOCK_UN)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def set_playbook(callback, playbook):
|
||||
''' used to notify callback plugins of playbook context '''
|
||||
callback.playbook = playbook
|
||||
for callback_plugin in callback_plugins:
|
||||
callback_plugin.playbook = playbook
|
||||
|
||||
def set_play(callback, play):
|
||||
''' used to notify callback plugins of context '''
|
||||
callback.play = play
|
||||
for callback_plugin in callback_plugins:
|
||||
callback_plugin.play = play
|
||||
|
||||
def set_task(callback, task):
|
||||
''' used to notify callback plugins of context '''
|
||||
callback.task = task
|
||||
for callback_plugin in callback_plugins:
|
||||
callback_plugin.task = task
|
||||
|
||||
def display(msg, color=None, stderr=False, screen_only=False, log_only=False, runner=None):
|
||||
# prevent a very rare case of interlaced multiprocess I/O
|
||||
log_flock(runner)
|
||||
msg2 = msg
|
||||
if color:
|
||||
msg2 = stringc(msg, color)
|
||||
if not log_only:
|
||||
if not stderr:
|
||||
try:
|
||||
print msg2
|
||||
except UnicodeEncodeError:
|
||||
print msg2.encode('utf-8')
|
||||
else:
|
||||
try:
|
||||
print >>sys.stderr, msg2
|
||||
except UnicodeEncodeError:
|
||||
print >>sys.stderr, msg2.encode('utf-8')
|
||||
if constants.DEFAULT_LOG_PATH != '':
|
||||
while msg.startswith("\n"):
|
||||
msg = msg.replace("\n","")
|
||||
if not screen_only:
|
||||
if color == 'red':
|
||||
logger.error(msg)
|
||||
else:
|
||||
logger.info(msg)
|
||||
log_unflock(runner)
|
||||
|
||||
def call_callback_module(method_name, *args, **kwargs):
|
||||
|
||||
for callback_plugin in callback_plugins:
|
||||
# a plugin that set self.disabled to True will not be called
|
||||
# see osx_say.py example for such a plugin
|
||||
if getattr(callback_plugin, 'disabled', False):
|
||||
continue
|
||||
methods = [
|
||||
getattr(callback_plugin, method_name, None),
|
||||
getattr(callback_plugin, 'on_any', None)
|
||||
]
|
||||
for method in methods:
|
||||
if method is not None:
|
||||
method(*args, **kwargs)
|
||||
|
||||
def vv(msg, host=None):
|
||||
return verbose(msg, host=host, caplevel=1)
|
||||
|
||||
def vvv(msg, host=None):
|
||||
return verbose(msg, host=host, caplevel=2)
|
||||
|
||||
def vvvv(msg, host=None):
|
||||
return verbose(msg, host=host, caplevel=3)
|
||||
|
||||
def verbose(msg, host=None, caplevel=2):
|
||||
msg = utils.sanitize_output(msg)
|
||||
if utils.VERBOSITY > caplevel:
|
||||
if host is None:
|
||||
display(msg, color='blue')
|
||||
else:
|
||||
display("<%s> %s" % (host, msg), color='blue')
|
||||
|
||||
class AggregateStats(object):
|
||||
''' holds stats about per-host activity during playbook runs '''
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.processed = {}
|
||||
self.failures = {}
|
||||
self.ok = {}
|
||||
self.dark = {}
|
||||
self.changed = {}
|
||||
self.skipped = {}
|
||||
|
||||
def _increment(self, what, host):
|
||||
''' helper function to bump a statistic '''
|
||||
|
||||
self.processed[host] = 1
|
||||
prev = (getattr(self, what)).get(host, 0)
|
||||
getattr(self, what)[host] = prev+1
|
||||
|
||||
def compute(self, runner_results, setup=False, poll=False, ignore_errors=False):
|
||||
''' walk through all results and increment stats '''
|
||||
|
||||
for (host, value) in runner_results.get('contacted', {}).iteritems():
|
||||
if not ignore_errors and (('failed' in value and bool(value['failed'])) or
|
||||
('failed_when_result' in value and [value['failed_when_result']] or ['rc' in value and value['rc'] != 0])[0]):
|
||||
self._increment('failures', host)
|
||||
elif 'skipped' in value and bool(value['skipped']):
|
||||
self._increment('skipped', host)
|
||||
elif 'changed' in value and bool(value['changed']):
|
||||
if not setup and not poll:
|
||||
self._increment('changed', host)
|
||||
self._increment('ok', host)
|
||||
else:
|
||||
if not poll or ('finished' in value and bool(value['finished'])):
|
||||
self._increment('ok', host)
|
||||
|
||||
for (host, value) in runner_results.get('dark', {}).iteritems():
|
||||
self._increment('dark', host)
|
||||
|
||||
|
||||
def summarize(self, host):
|
||||
''' return information about a particular host '''
|
||||
|
||||
return dict(
|
||||
ok = self.ok.get(host, 0),
|
||||
failures = self.failures.get(host, 0),
|
||||
unreachable = self.dark.get(host,0),
|
||||
changed = self.changed.get(host, 0),
|
||||
skipped = self.skipped.get(host, 0)
|
||||
)
|
||||
|
||||
########################################################################
|
||||
|
||||
def regular_generic_msg(hostname, result, oneline, caption):
|
||||
''' output on the result of a module run that is not command '''
|
||||
|
||||
if not oneline:
|
||||
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result,format=True))
|
||||
else:
|
||||
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result))
|
||||
|
||||
|
||||
def banner_cowsay(msg):
|
||||
|
||||
if ": [" in msg:
|
||||
msg = msg.replace("[","")
|
||||
if msg.endswith("]"):
|
||||
msg = msg[:-1]
|
||||
runcmd = [cowsay,"-W", "60"]
|
||||
if noncow:
|
||||
runcmd.append('-f')
|
||||
runcmd.append(noncow)
|
||||
runcmd.append(msg)
|
||||
cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
return "%s\n" % out
|
||||
|
||||
def banner_normal(msg):
|
||||
|
||||
width = 78 - len(msg)
|
||||
if width < 3:
|
||||
width = 3
|
||||
filler = "*" * width
|
||||
return "\n%s %s " % (msg, filler)
|
||||
|
||||
def banner(msg):
|
||||
if cowsay:
|
||||
try:
|
||||
return banner_cowsay(msg)
|
||||
except OSError:
|
||||
# somebody cleverly deleted cowsay or something during the PB run. heh.
|
||||
return banner_normal(msg)
|
||||
return banner_normal(msg)
|
||||
|
||||
def command_generic_msg(hostname, result, oneline, caption):
|
||||
''' output the result of a command run '''
|
||||
|
||||
rc = result.get('rc', '0')
|
||||
stdout = result.get('stdout','')
|
||||
stderr = result.get('stderr', '')
|
||||
msg = result.get('msg', '')
|
||||
|
||||
hostname = hostname.encode('utf-8')
|
||||
caption = caption.encode('utf-8')
|
||||
|
||||
if not oneline:
|
||||
buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0))
|
||||
if stdout:
|
||||
buf += stdout
|
||||
if stderr:
|
||||
buf += stderr
|
||||
if msg:
|
||||
buf += msg
|
||||
return buf + "\n"
|
||||
else:
|
||||
if stderr:
|
||||
return "%s | %s | rc=%s | (stdout) %s (stderr) %s" % (hostname, caption, rc, stdout, stderr)
|
||||
else:
|
||||
return "%s | %s | rc=%s | (stdout) %s" % (hostname, caption, rc, stdout)
|
||||
|
||||
def host_report_msg(hostname, module_name, result, oneline):
|
||||
''' summarize the JSON results for a particular host '''
|
||||
|
||||
failed = utils.is_failed(result)
|
||||
msg = ('', None)
|
||||
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result and result.get('parsed',True) != False:
|
||||
if not failed:
|
||||
msg = (command_generic_msg(hostname, result, oneline, 'success'), 'green')
|
||||
else:
|
||||
msg = (command_generic_msg(hostname, result, oneline, 'FAILED'), 'red')
|
||||
else:
|
||||
if not failed:
|
||||
msg = (regular_generic_msg(hostname, result, oneline, 'success'), 'green')
|
||||
else:
|
||||
msg = (regular_generic_msg(hostname, result, oneline, 'FAILED'), 'red')
|
||||
return msg
|
||||
|
||||
###############################################
|
||||
|
||||
class DefaultRunnerCallbacks(object):
|
||||
''' no-op callbacks for API usage of Runner() if no callbacks are specified '''
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def on_failed(self, host, res, ignore_errors=False):
|
||||
call_callback_module('runner_on_failed', host, res, ignore_errors=ignore_errors)
|
||||
|
||||
def on_ok(self, host, res):
|
||||
call_callback_module('runner_on_ok', host, res)
|
||||
|
||||
def on_skipped(self, host, item=None):
|
||||
call_callback_module('runner_on_skipped', host, item=item)
|
||||
|
||||
def on_unreachable(self, host, res):
|
||||
call_callback_module('runner_on_unreachable', host, res)
|
||||
|
||||
def on_no_hosts(self):
|
||||
call_callback_module('runner_on_no_hosts')
|
||||
|
||||
def on_async_poll(self, host, res, jid, clock):
|
||||
call_callback_module('runner_on_async_poll', host, res, jid, clock)
|
||||
|
||||
def on_async_ok(self, host, res, jid):
|
||||
call_callback_module('runner_on_async_ok', host, res, jid)
|
||||
|
||||
def on_async_failed(self, host, res, jid):
|
||||
call_callback_module('runner_on_async_failed', host, res, jid)
|
||||
|
||||
def on_file_diff(self, host, diff):
|
||||
call_callback_module('runner_on_file_diff', host, diff)
|
||||
|
||||
########################################################################
|
||||
|
||||
class CliRunnerCallbacks(DefaultRunnerCallbacks):
|
||||
''' callbacks for use by /usr/bin/ansible '''
|
||||
|
||||
def __init__(self):
|
||||
# set by /usr/bin/ansible later
|
||||
self.options = None
|
||||
self._async_notified = {}
|
||||
|
||||
def on_failed(self, host, res, ignore_errors=False):
|
||||
self._on_any(host,res)
|
||||
super(CliRunnerCallbacks, self).on_failed(host, res, ignore_errors=ignore_errors)
|
||||
|
||||
def on_ok(self, host, res):
|
||||
# hide magic variables used for ansible-playbook
|
||||
res.pop('verbose_override', None)
|
||||
res.pop('verbose_always', None)
|
||||
|
||||
self._on_any(host,res)
|
||||
super(CliRunnerCallbacks, self).on_ok(host, res)
|
||||
|
||||
def on_unreachable(self, host, res):
|
||||
if type(res) == dict:
|
||||
res = res.get('msg','')
|
||||
display("%s | FAILED => %s" % (host, res), stderr=True, color='red', runner=self.runner)
|
||||
if self.options.tree:
|
||||
utils.write_tree_file(
|
||||
self.options.tree, host,
|
||||
utils.jsonify(dict(failed=True, msg=res),format=True)
|
||||
)
|
||||
super(CliRunnerCallbacks, self).on_unreachable(host, res)
|
||||
|
||||
def on_skipped(self, host, item=None):
|
||||
display("%s | skipped" % (host), runner=self.runner)
|
||||
super(CliRunnerCallbacks, self).on_skipped(host, item)
|
||||
|
||||
def on_no_hosts(self):
|
||||
display("no hosts matched\n", stderr=True, runner=self.runner)
|
||||
super(CliRunnerCallbacks, self).on_no_hosts()
|
||||
|
||||
def on_async_poll(self, host, res, jid, clock):
|
||||
if jid not in self._async_notified:
|
||||
self._async_notified[jid] = clock + 1
|
||||
if self._async_notified[jid] > clock:
|
||||
self._async_notified[jid] = clock
|
||||
display("<job %s> polling on %s, %ss remaining" % (jid, host, clock), runner=self.runner)
|
||||
super(CliRunnerCallbacks, self).on_async_poll(host, res, jid, clock)
|
||||
|
||||
def on_async_ok(self, host, res, jid):
|
||||
if jid:
|
||||
display("<job %s> finished on %s => %s"%(jid, host, utils.jsonify(res,format=True)), runner=self.runner)
|
||||
super(CliRunnerCallbacks, self).on_async_ok(host, res, jid)
|
||||
|
||||
def on_async_failed(self, host, res, jid):
|
||||
display("<job %s> FAILED on %s => %s"%(jid, host, utils.jsonify(res,format=True)), color='red', stderr=True, runner=self.runner)
|
||||
super(CliRunnerCallbacks, self).on_async_failed(host,res,jid)
|
||||
|
||||
def _on_any(self, host, result):
|
||||
result2 = result.copy()
|
||||
result2.pop('invocation', None)
|
||||
(msg, color) = host_report_msg(host, self.options.module_name, result2, self.options.one_line)
|
||||
display(msg, color=color, runner=self.runner)
|
||||
if self.options.tree:
|
||||
utils.write_tree_file(self.options.tree, host, utils.jsonify(result2,format=True))
|
||||
|
||||
def on_file_diff(self, host, diff):
|
||||
display(utils.get_diff(diff), runner=self.runner)
|
||||
super(CliRunnerCallbacks, self).on_file_diff(host, diff)
|
||||
|
||||
########################################################################
|
||||
|
||||
class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
|
||||
''' callbacks used for Runner() from /usr/bin/ansible-playbook '''
|
||||
|
||||
def __init__(self, stats, verbose=None):
|
||||
|
||||
if verbose is None:
|
||||
verbose = utils.VERBOSITY
|
||||
|
||||
self.verbose = verbose
|
||||
self.stats = stats
|
||||
self._async_notified = {}
|
||||
|
||||
def on_unreachable(self, host, results):
|
||||
if self.runner.delegate_to:
|
||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
||||
|
||||
item = None
|
||||
if type(results) == dict:
|
||||
item = results.get('item', None)
|
||||
if isinstance(item, unicode):
|
||||
item = utils.unicode.to_bytes(item)
|
||||
results = basic.json_dict_unicode_to_bytes(results)
|
||||
else:
|
||||
results = utils.unicode.to_bytes(results)
|
||||
host = utils.unicode.to_bytes(host)
|
||||
if item:
|
||||
msg = "fatal: [%s] => (item=%s) => %s" % (host, item, results)
|
||||
else:
|
||||
msg = "fatal: [%s] => %s" % (host, results)
|
||||
display(msg, color='red', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_unreachable(host, results)
|
||||
|
||||
def on_failed(self, host, results, ignore_errors=False):
|
||||
if self.runner.delegate_to:
|
||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
||||
|
||||
results2 = results.copy()
|
||||
results2.pop('invocation', None)
|
||||
|
||||
item = results2.get('item', None)
|
||||
parsed = results2.get('parsed', True)
|
||||
module_msg = ''
|
||||
if not parsed:
|
||||
module_msg = results2.pop('msg', None)
|
||||
stderr = results2.pop('stderr', None)
|
||||
stdout = results2.pop('stdout', None)
|
||||
returned_msg = results2.pop('msg', None)
|
||||
|
||||
results2['task'] = self.task.name
|
||||
results2['role'] = self.task.role_name
|
||||
results2['playbook'] = self.playbook.filename
|
||||
|
||||
if item:
|
||||
msg = "failed: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(results2))
|
||||
else:
|
||||
msg = "failed: [%s] => %s" % (host, utils.jsonify(results2))
|
||||
display(msg, color='red', runner=self.runner)
|
||||
|
||||
if stderr:
|
||||
display("stderr: %s" % stderr, color='red', runner=self.runner)
|
||||
if stdout:
|
||||
display("stdout: %s" % stdout, color='red', runner=self.runner)
|
||||
if returned_msg:
|
||||
display("msg: %s" % returned_msg, color='red', runner=self.runner)
|
||||
if not parsed and module_msg:
|
||||
display(module_msg, color='red', runner=self.runner)
|
||||
if ignore_errors:
|
||||
display("...ignoring", color='cyan', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_failed(host, results, ignore_errors=ignore_errors)
|
||||
|
||||
def on_ok(self, host, host_result):
|
||||
if self.runner.delegate_to:
|
||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
||||
|
||||
item = host_result.get('item', None)
|
||||
|
||||
host_result2 = host_result.copy()
|
||||
host_result2.pop('invocation', None)
|
||||
verbose_always = host_result2.pop('verbose_always', False)
|
||||
changed = host_result.get('changed', False)
|
||||
ok_or_changed = 'ok'
|
||||
if changed:
|
||||
ok_or_changed = 'changed'
|
||||
|
||||
# show verbose output for non-setup module results if --verbose is used
|
||||
msg = ''
|
||||
if (not self.verbose or host_result2.get("verbose_override",None) is not
|
||||
None) and not verbose_always:
|
||||
if item:
|
||||
msg = "%s: [%s] => (item=%s)" % (ok_or_changed, host, item)
|
||||
else:
|
||||
if 'ansible_job_id' not in host_result or 'finished' in host_result:
|
||||
msg = "%s: [%s]" % (ok_or_changed, host)
|
||||
else:
|
||||
# verbose ...
|
||||
if item:
|
||||
msg = "%s: [%s] => (item=%s) => %s" % (ok_or_changed, host, item, utils.jsonify(host_result2, format=verbose_always))
|
||||
else:
|
||||
if 'ansible_job_id' not in host_result or 'finished' in host_result2:
|
||||
msg = "%s: [%s] => %s" % (ok_or_changed, host, utils.jsonify(host_result2, format=verbose_always))
|
||||
|
||||
if msg != '':
|
||||
if not changed:
|
||||
display(msg, color='green', runner=self.runner)
|
||||
else:
|
||||
display(msg, color='yellow', runner=self.runner)
|
||||
if constants.COMMAND_WARNINGS and 'warnings' in host_result2 and host_result2['warnings']:
|
||||
for warning in host_result2['warnings']:
|
||||
display("warning: %s" % warning, color='purple', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_ok(host, host_result)
|
||||
|
||||
def on_skipped(self, host, item=None):
|
||||
if self.runner.delegate_to:
|
||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
||||
|
||||
if constants.DISPLAY_SKIPPED_HOSTS:
|
||||
msg = ''
|
||||
if item:
|
||||
msg = "skipping: [%s] => (item=%s)" % (host, item)
|
||||
else:
|
||||
msg = "skipping: [%s]" % host
|
||||
display(msg, color='cyan', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_skipped(host, item)
|
||||
|
||||
def on_no_hosts(self):
|
||||
display("FATAL: no hosts matched or all hosts have already failed -- aborting\n", color='red', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_no_hosts()
|
||||
|
||||
def on_async_poll(self, host, res, jid, clock):
|
||||
if jid not in self._async_notified:
|
||||
self._async_notified[jid] = clock + 1
|
||||
if self._async_notified[jid] > clock:
|
||||
self._async_notified[jid] = clock
|
||||
msg = "<job %s> polling, %ss remaining"%(jid, clock)
|
||||
display(msg, color='cyan', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_async_poll(host,res,jid,clock)
|
||||
|
||||
def on_async_ok(self, host, res, jid):
|
||||
if jid:
|
||||
msg = "<job %s> finished on %s"%(jid, host)
|
||||
display(msg, color='cyan', runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_async_ok(host, res, jid)
|
||||
|
||||
def on_async_failed(self, host, res, jid):
|
||||
msg = "<job %s> FAILED on %s" % (jid, host)
|
||||
display(msg, color='red', stderr=True, runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_async_failed(host,res,jid)
|
||||
|
||||
def on_file_diff(self, host, diff):
|
||||
display(utils.get_diff(diff), runner=self.runner)
|
||||
super(PlaybookRunnerCallbacks, self).on_file_diff(host, diff)
|
||||
|
||||
########################################################################
|
||||
|
||||
class PlaybookCallbacks(object):
|
||||
''' playbook.py callbacks used by /usr/bin/ansible-playbook '''
|
||||
|
||||
def __init__(self, verbose=False):
|
||||
|
||||
self.verbose = verbose
|
||||
|
||||
def on_start(self):
|
||||
call_callback_module('playbook_on_start')
|
||||
|
||||
def on_notify(self, host, handler):
|
||||
call_callback_module('playbook_on_notify', host, handler)
|
||||
|
||||
def on_no_hosts_matched(self):
|
||||
display("skipping: no hosts matched", color='cyan')
|
||||
call_callback_module('playbook_on_no_hosts_matched')
|
||||
|
||||
def on_no_hosts_remaining(self):
|
||||
display("\nFATAL: all hosts have already failed -- aborting", color='red')
|
||||
call_callback_module('playbook_on_no_hosts_remaining')
|
||||
|
||||
def on_task_start(self, name, is_conditional):
|
||||
name = utils.unicode.to_bytes(name)
|
||||
msg = "TASK: [%s]" % name
|
||||
if is_conditional:
|
||||
msg = "NOTIFIED: [%s]" % name
|
||||
|
||||
if hasattr(self, 'start_at'):
|
||||
self.start_at = utils.unicode.to_bytes(self.start_at)
|
||||
if name == self.start_at or fnmatch.fnmatch(name, self.start_at):
|
||||
# we found out match, we can get rid of this now
|
||||
del self.start_at
|
||||
elif self.task.role_name:
|
||||
# handle tasks prefixed with rolenames
|
||||
actual_name = name.split('|', 1)[1].lstrip()
|
||||
if actual_name == self.start_at or fnmatch.fnmatch(actual_name, self.start_at):
|
||||
del self.start_at
|
||||
|
||||
if hasattr(self, 'start_at'): # we still have start_at so skip the task
|
||||
self.skip_task = True
|
||||
elif hasattr(self, 'step') and self.step:
|
||||
if isinstance(name, str):
|
||||
name = utils.unicode.to_unicode(name)
|
||||
msg = u'Perform task: %s (y/n/c): ' % name
|
||||
if sys.stdout.encoding:
|
||||
msg = to_bytes(msg, sys.stdout.encoding)
|
||||
else:
|
||||
msg = to_bytes(msg)
|
||||
resp = raw_input(msg)
|
||||
if resp.lower() in ['y','yes']:
|
||||
self.skip_task = False
|
||||
display(banner(msg))
|
||||
elif resp.lower() in ['c', 'continue']:
|
||||
self.skip_task = False
|
||||
self.step = False
|
||||
display(banner(msg))
|
||||
else:
|
||||
self.skip_task = True
|
||||
else:
|
||||
self.skip_task = False
|
||||
display(banner(msg))
|
||||
|
||||
call_callback_module('playbook_on_task_start', name, is_conditional)
|
||||
|
||||
def on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
|
||||
|
||||
if prompt and default is not None:
|
||||
msg = "%s [%s]: " % (prompt, default)
|
||||
elif prompt:
|
||||
msg = "%s: " % prompt
|
||||
else:
|
||||
msg = 'input for %s: ' % varname
|
||||
|
||||
def do_prompt(prompt, private):
|
||||
if sys.stdout.encoding:
|
||||
msg = prompt.encode(sys.stdout.encoding)
|
||||
else:
|
||||
# when piping the output, or at other times when stdout
|
||||
# may not be the standard file descriptor, the stdout
|
||||
# encoding may not be set, so default to something sane
|
||||
msg = prompt.encode(locale.getpreferredencoding())
|
||||
if private:
|
||||
return getpass.getpass(msg)
|
||||
return raw_input(msg)
|
||||
|
||||
|
||||
if confirm:
|
||||
while True:
|
||||
result = do_prompt(msg, private)
|
||||
second = do_prompt("confirm " + msg, private)
|
||||
if result == second:
|
||||
break
|
||||
display("***** VALUES ENTERED DO NOT MATCH ****")
|
||||
else:
|
||||
result = do_prompt(msg, private)
|
||||
|
||||
# if result is false and default is not None
|
||||
if not result and default is not None:
|
||||
result = default
|
||||
|
||||
|
||||
if encrypt:
|
||||
result = utils.do_encrypt(result, encrypt, salt_size, salt)
|
||||
|
||||
# handle utf-8 chars
|
||||
result = to_unicode(result, errors='strict')
|
||||
call_callback_module( 'playbook_on_vars_prompt', varname, private=private, prompt=prompt,
|
||||
encrypt=encrypt, confirm=confirm, salt_size=salt_size, salt=None, default=default
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def on_setup(self):
|
||||
display(banner("GATHERING FACTS"))
|
||||
call_callback_module('playbook_on_setup')
|
||||
|
||||
def on_import_for_host(self, host, imported_file):
|
||||
msg = "%s: importing %s" % (host, imported_file)
|
||||
display(msg, color='cyan')
|
||||
call_callback_module('playbook_on_import_for_host', host, imported_file)
|
||||
|
||||
def on_not_import_for_host(self, host, missing_file):
|
||||
msg = "%s: not importing file: %s" % (host, missing_file)
|
||||
display(msg, color='cyan')
|
||||
call_callback_module('playbook_on_not_import_for_host', host, missing_file)
|
||||
|
||||
def on_play_start(self, name):
|
||||
display(banner("PLAY [%s]" % name))
|
||||
call_callback_module('playbook_on_play_start', name)
|
||||
|
||||
def on_stats(self, stats):
|
||||
call_callback_module('playbook_on_stats', stats)
|
||||
|
||||
|
||||
447
lib/ansible/cli/__init__.py
Normal file
447
lib/ansible/cli/__init__.py
Normal file
@@ -0,0 +1,447 @@
|
||||
# (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 operator
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
import re
|
||||
import getpass
|
||||
import subprocess
|
||||
|
||||
from ansible import __version__
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.utils.unicode import to_bytes
|
||||
|
||||
class SortedOptParser(optparse.OptionParser):
|
||||
'''Optparser which sorts the options by opt before outputting --help'''
|
||||
|
||||
#FIXME: epilog parsing: OptionParser.format_epilog = lambda self, formatter: self.epilog
|
||||
|
||||
def format_help(self, formatter=None, epilog=None):
|
||||
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
|
||||
return optparse.OptionParser.format_help(self, formatter=None)
|
||||
|
||||
class CLI(object):
|
||||
''' code behind bin/ansible* programs '''
|
||||
|
||||
VALID_ACTIONS = ['No Actions']
|
||||
|
||||
_ITALIC = re.compile(r"I\(([^)]+)\)")
|
||||
_BOLD = re.compile(r"B\(([^)]+)\)")
|
||||
_MODULE = re.compile(r"M\(([^)]+)\)")
|
||||
_URL = re.compile(r"U\(([^)]+)\)")
|
||||
_CONST = re.compile(r"C\(([^)]+)\)")
|
||||
|
||||
PAGER = 'less'
|
||||
LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars)
|
||||
# -S (chop long lines) -X (disable termcap init and de-init)
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
"""
|
||||
Base init method for all command line programs
|
||||
"""
|
||||
|
||||
self.args = args
|
||||
self.options = None
|
||||
self.parser = None
|
||||
self.action = None
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
|
||||
def set_action(self):
|
||||
"""
|
||||
Get the action the user wants to execute from the sys argv list.
|
||||
"""
|
||||
for i in range(0,len(self.args)):
|
||||
arg = self.args[i]
|
||||
if arg in self.VALID_ACTIONS:
|
||||
self.action = arg
|
||||
del self.args[i]
|
||||
break
|
||||
|
||||
if not self.action:
|
||||
raise AnsibleOptionsError("Missing required action")
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
Actually runs a child defined method using the execute_<action> pattern
|
||||
"""
|
||||
fn = getattr(self, "execute_%s" % self.action)
|
||||
fn()
|
||||
|
||||
def parse(self):
|
||||
raise Exception("Need to implement!")
|
||||
|
||||
def run(self):
|
||||
raise Exception("Need to implement!")
|
||||
|
||||
@staticmethod
|
||||
def ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=False, confirm_vault=False, confirm_new=False):
|
||||
''' prompt for vault password and/or password change '''
|
||||
|
||||
vault_pass = None
|
||||
new_vault_pass = None
|
||||
|
||||
if ask_vault_pass:
|
||||
vault_pass = getpass.getpass(prompt="Vault password: ")
|
||||
|
||||
if ask_vault_pass and confirm_vault:
|
||||
vault_pass2 = getpass.getpass(prompt="Confirm Vault password: ")
|
||||
if vault_pass != vault_pass2:
|
||||
raise errors.AnsibleError("Passwords do not match")
|
||||
|
||||
if ask_new_vault_pass:
|
||||
new_vault_pass = getpass.getpass(prompt="New Vault password: ")
|
||||
|
||||
if ask_new_vault_pass and confirm_new:
|
||||
new_vault_pass2 = getpass.getpass(prompt="Confirm New Vault password: ")
|
||||
if new_vault_pass != new_vault_pass2:
|
||||
raise errors.AnsibleError("Passwords do not match")
|
||||
|
||||
# enforce no newline chars at the end of passwords
|
||||
if vault_pass:
|
||||
vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip()
|
||||
if new_vault_pass:
|
||||
new_vault_pass = to_bytes(new_vault_pass, errors='strict', nonstring='simplerepr').strip()
|
||||
|
||||
return vault_pass, new_vault_pass
|
||||
|
||||
|
||||
def ask_passwords(self):
|
||||
''' prompt for connection and become passwords if needed '''
|
||||
|
||||
op = self.options
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
become_prompt = ''
|
||||
|
||||
if op.ask_pass:
|
||||
sshpass = getpass.getpass(prompt="SSH password: ")
|
||||
become_prompt = "%s password[defaults to SSH password]: " % op.become_method.upper()
|
||||
if sshpass:
|
||||
sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
|
||||
else:
|
||||
become_prompt = "%s password: " % op.become_method.upper()
|
||||
|
||||
if op.become_ask_pass:
|
||||
becomepass = getpass.getpass(prompt=become_prompt)
|
||||
if op.ask_pass and becomepass == '':
|
||||
becomepass = sshpass
|
||||
if becomepass:
|
||||
becomepass = to_bytes(becomepass)
|
||||
|
||||
return (sshpass, becomepass)
|
||||
|
||||
|
||||
def normalize_become_options(self):
|
||||
''' this keeps backwards compatibility with sudo/su self.options '''
|
||||
self.options.become_ask_pass = self.options.become_ask_pass or self.options.ask_sudo_pass or self.options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
|
||||
self.options.become_user = self.options.become_user or self.options.sudo_user or self.options.su_user or C.DEFAULT_BECOME_USER
|
||||
|
||||
if self.options.become:
|
||||
pass
|
||||
elif self.options.sudo:
|
||||
self.options.become = True
|
||||
self.options.become_method = 'sudo'
|
||||
elif self.options.su:
|
||||
self.options.become = True
|
||||
options.become_method = 'su'
|
||||
|
||||
|
||||
def validate_conflicts(self):
|
||||
''' check for conflicting options '''
|
||||
|
||||
op = self.options
|
||||
|
||||
# Check for vault related conflicts
|
||||
if (op.ask_vault_pass and op.vault_password_file):
|
||||
self.parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
|
||||
|
||||
|
||||
# Check for privilege escalation conflicts
|
||||
if (op.su or op.su_user or op.ask_su_pass) and \
|
||||
(op.sudo or op.sudo_user or op.ask_sudo_pass) or \
|
||||
(op.su or op.su_user or op.ask_su_pass) and \
|
||||
(op.become or op.become_user or op.become_ask_pass) or \
|
||||
(op.sudo or op.sudo_user or op.ask_sudo_pass) and \
|
||||
(op.become or op.become_user or op.become_ask_pass):
|
||||
|
||||
self.parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||
"and su arguments ('-su', '--su-user', and '--ask-su-pass') "
|
||||
"and become arguments ('--become', '--become-user', and '--ask-become-pass')"
|
||||
" are exclusive of each other")
|
||||
|
||||
@staticmethod
|
||||
def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, runtask_opts=False, vault_opts=False,
|
||||
async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False, epilog=None):
|
||||
''' create an options parser for most ansible scripts '''
|
||||
|
||||
#FIXME: implemente epilog parsing
|
||||
#OptionParser.format_epilog = lambda self, formatter: self.epilog
|
||||
|
||||
# base opts
|
||||
parser = SortedOptParser(usage, version=CLI.version("%prog"))
|
||||
parser.add_option('-v','--verbose', dest='verbosity', default=0, action="count",
|
||||
help="verbose mode (-vvv for more, -vvvv to enable connection debugging)")
|
||||
|
||||
if runtask_opts:
|
||||
parser.add_option('-f','--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
|
||||
help="specify number of parallel processes to use (default=%s)" % C.DEFAULT_FORKS)
|
||||
parser.add_option('-i', '--inventory-file', dest='inventory',
|
||||
help="specify inventory host file (default=%s)" % C.DEFAULT_HOST_LIST,
|
||||
default=C.DEFAULT_HOST_LIST)
|
||||
parser.add_option('--list-hosts', dest='listhosts', action='store_true',
|
||||
help='outputs a list of matching hosts; does not execute anything else')
|
||||
parser.add_option('-M', '--module-path', dest='module_path',
|
||||
help="specify path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH, default=None)
|
||||
parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
|
||||
help="set additional variables as key=value or YAML/JSON", default=[])
|
||||
|
||||
if vault_opts:
|
||||
parser.add_option('--ask-vault-pass', default=False, dest='ask_vault_pass', action='store_true',
|
||||
help='ask for vault password')
|
||||
parser.add_option('--vault-password-file', default=C.DEFAULT_VAULT_PASSWORD_FILE,
|
||||
dest='vault_password_file', help="vault password file")
|
||||
|
||||
|
||||
if subset_opts:
|
||||
parser.add_option('-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset',
|
||||
help='further limit selected hosts to an additional pattern')
|
||||
parser.add_option('-t', '--tags', dest='tags', default='all',
|
||||
help="only run plays and tasks tagged with these values")
|
||||
parser.add_option('--skip-tags', dest='skip_tags',
|
||||
help="only run plays and tasks whose tags do not match these values")
|
||||
|
||||
if output_opts:
|
||||
parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
|
||||
help='condense output')
|
||||
parser.add_option('-t', '--tree', dest='tree', default=None,
|
||||
help='log output to this directory')
|
||||
|
||||
if runas_opts:
|
||||
# priv user defaults to root later on to enable detecting when this option was given here
|
||||
parser.add_option('-K', '--ask-sudo-pass', default=C.DEFAULT_ASK_SUDO_PASS, dest='ask_sudo_pass', action='store_true',
|
||||
help='ask for sudo password (deprecated, use become)')
|
||||
parser.add_option('--ask-su-pass', default=C.DEFAULT_ASK_SU_PASS, dest='ask_su_pass', action='store_true',
|
||||
help='ask for su password (deprecated, use become)')
|
||||
parser.add_option("-s", "--sudo", default=C.DEFAULT_SUDO, action="store_true", dest='sudo',
|
||||
help="run operations with sudo (nopasswd) (deprecated, use become)")
|
||||
parser.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
|
||||
help='desired sudo user (default=root) (deprecated, use become)')
|
||||
parser.add_option('-S', '--su', default=C.DEFAULT_SU, action='store_true',
|
||||
help='run operations with su (deprecated, use become)')
|
||||
parser.add_option('-R', '--su-user', default=None,
|
||||
help='run operations with su as this user (default=%s) (deprecated, use become)' % C.DEFAULT_SU_USER)
|
||||
|
||||
# consolidated privilege escalation (become)
|
||||
parser.add_option("-b", "--become", default=C.DEFAULT_BECOME, action="store_true", dest='become',
|
||||
help="run operations with become (nopasswd implied)")
|
||||
parser.add_option('--become-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, type='string',
|
||||
help="privilege escalation method to use (default=%s), valid choices: [ %s ]" % (C.DEFAULT_BECOME_METHOD, ' | '.join(C.BECOME_METHODS)))
|
||||
parser.add_option('--become-user', default=None, dest='become_user', type='string',
|
||||
help='run operations as this user (default=%s)' % C.DEFAULT_BECOME_USER)
|
||||
parser.add_option('--ask-become-pass', default=False, dest='become_ask_pass', action='store_true',
|
||||
help='ask for privilege escalation password')
|
||||
|
||||
|
||||
if connect_opts:
|
||||
parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
|
||||
help='ask for connection password')
|
||||
parser.add_option('--private-key', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file',
|
||||
help='use this file to authenticate the connection')
|
||||
parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user',
|
||||
help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER)
|
||||
parser.add_option('-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT,
|
||||
help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
|
||||
parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout',
|
||||
help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT)
|
||||
|
||||
|
||||
if async_opts:
|
||||
parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int',
|
||||
dest='poll_interval',
|
||||
help="set the poll interval if using -B (default=%s)" % C.DEFAULT_POLL_INTERVAL)
|
||||
parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
|
||||
help='run asynchronously, failing after X seconds (default=N/A)')
|
||||
|
||||
if check_opts:
|
||||
parser.add_option("-C", "--check", default=False, dest='check', action='store_true',
|
||||
help="don't make any changes; instead, try to predict some of the changes that may occur")
|
||||
parser.add_option('--syntax-check', dest='syntax', action='store_true',
|
||||
help="perform a syntax check on the playbook, but do not execute it")
|
||||
|
||||
if diff_opts:
|
||||
parser.add_option("-D", "--diff", default=False, dest='diff', action='store_true',
|
||||
help="when changing (small) files and templates, show the differences in those files; works great with --check"
|
||||
)
|
||||
|
||||
if meta_opts:
|
||||
parser.add_option('--force-handlers', dest='force_handlers', action='store_true',
|
||||
help="run handlers even if a task fails")
|
||||
parser.add_option('--flush-cache', dest='flush_cache', action='store_true',
|
||||
help="clear the fact cache")
|
||||
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def version(prog):
|
||||
''' return ansible version '''
|
||||
result = "{0} {1}".format(prog, __version__)
|
||||
gitinfo = CLI._gitinfo()
|
||||
if gitinfo:
|
||||
result = result + " {0}".format(gitinfo)
|
||||
result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def version_info(gitinfo=False):
|
||||
''' return full ansible version info '''
|
||||
if gitinfo:
|
||||
# expensive call, user with care
|
||||
ansible_version_string = version('')
|
||||
else:
|
||||
ansible_version_string = __version__
|
||||
ansible_version = ansible_version_string.split()[0]
|
||||
ansible_versions = ansible_version.split('.')
|
||||
for counter in range(len(ansible_versions)):
|
||||
if ansible_versions[counter] == "":
|
||||
ansible_versions[counter] = 0
|
||||
try:
|
||||
ansible_versions[counter] = int(ansible_versions[counter])
|
||||
except:
|
||||
pass
|
||||
if len(ansible_versions) < 3:
|
||||
for counter in range(len(ansible_versions), 3):
|
||||
ansible_versions.append(0)
|
||||
return {'string': ansible_version_string.strip(),
|
||||
'full': ansible_version,
|
||||
'major': ansible_versions[0],
|
||||
'minor': ansible_versions[1],
|
||||
'revision': ansible_versions[2]}
|
||||
|
||||
@staticmethod
|
||||
def _git_repo_info(repo_path):
|
||||
''' returns a string containing git branch, commit id and commit date '''
|
||||
result = None
|
||||
if os.path.exists(repo_path):
|
||||
# Check if the .git is a file. If it is a file, it means that we are in a submodule structure.
|
||||
if os.path.isfile(repo_path):
|
||||
try:
|
||||
gitdir = yaml.safe_load(open(repo_path)).get('gitdir')
|
||||
# There is a possibility the .git file to have an absolute path.
|
||||
if os.path.isabs(gitdir):
|
||||
repo_path = gitdir
|
||||
else:
|
||||
repo_path = os.path.join(repo_path[:-4], gitdir)
|
||||
except (IOError, AttributeError):
|
||||
return ''
|
||||
f = open(os.path.join(repo_path, "HEAD"))
|
||||
branch = f.readline().split('/')[-1].rstrip("\n")
|
||||
f.close()
|
||||
branch_path = os.path.join(repo_path, "refs", "heads", branch)
|
||||
if os.path.exists(branch_path):
|
||||
f = open(branch_path)
|
||||
commit = f.readline()[:10]
|
||||
f.close()
|
||||
else:
|
||||
# detached HEAD
|
||||
commit = branch[:10]
|
||||
branch = 'detached HEAD'
|
||||
branch_path = os.path.join(repo_path, "HEAD")
|
||||
|
||||
date = time.localtime(os.stat(branch_path).st_mtime)
|
||||
if time.daylight == 0:
|
||||
offset = time.timezone
|
||||
else:
|
||||
offset = time.altzone
|
||||
result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit,
|
||||
time.strftime("%Y/%m/%d %H:%M:%S", date), int(offset / -36))
|
||||
else:
|
||||
result = ''
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _gitinfo():
|
||||
basedir = os.path.join(os.path.dirname(__file__), '..', '..', '..')
|
||||
repo_path = os.path.join(basedir, '.git')
|
||||
result = CLI._git_repo_info(repo_path)
|
||||
submodules = os.path.join(basedir, '.gitmodules')
|
||||
if not os.path.exists(submodules):
|
||||
return result
|
||||
f = open(submodules)
|
||||
for line in f:
|
||||
tokens = line.strip().split(' ')
|
||||
if tokens[0] == 'path':
|
||||
submodule_path = tokens[2]
|
||||
submodule_info = CLI._git_repo_info(os.path.join(basedir, submodule_path, '.git'))
|
||||
if not submodule_info:
|
||||
submodule_info = ' not found - use git submodule update --init ' + submodule_path
|
||||
result += "\n {0}: {1}".format(submodule_path, submodule_info)
|
||||
f.close()
|
||||
return result
|
||||
|
||||
|
||||
@staticmethod
|
||||
def pager(text):
|
||||
''' find reasonable way to display text '''
|
||||
# this is a much simpler form of what is in pydoc.py
|
||||
if not sys.stdout.isatty():
|
||||
pager_print(text)
|
||||
elif 'PAGER' in os.environ:
|
||||
if sys.platform == 'win32':
|
||||
pager_print(text)
|
||||
else:
|
||||
CLI.pager_pipe(text, os.environ['PAGER'])
|
||||
elif subprocess.call('(less --version) 2> /dev/null', shell = True) == 0:
|
||||
CLI.pager_pipe(text, 'less')
|
||||
else:
|
||||
pager_print(text)
|
||||
|
||||
@staticmethod
|
||||
def pager_pipe(text, cmd):
|
||||
''' pipe text through a pager '''
|
||||
if 'LESS' not in os.environ:
|
||||
os.environ['LESS'] = CLI.LESS_OPTS
|
||||
try:
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
|
||||
cmd.communicate(input=text)
|
||||
except IOError:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def tty_ify(self, text):
|
||||
|
||||
t = self._ITALIC.sub("`" + r"\1" + "'", text) # I(word) => `word'
|
||||
t = self._BOLD.sub("*" + r"\1" + "*", t) # B(word) => *word*
|
||||
t = self._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word]
|
||||
t = self._URL.sub(r"\1", t) # U(word) => word
|
||||
t = self._CONST.sub("`" + r"\1" + "'", t) # C(word) => `word'
|
||||
|
||||
return t
|
||||
157
lib/ansible/cli/adhoc.py
Normal file
157
lib/ansible/cli/adhoc.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# (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 ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.executor.task_queue_manager import TaskQueueManager
|
||||
from ansible.inventory import Inventory
|
||||
from ansible.parsing import DataLoader
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.playbook.play import Play
|
||||
from ansible.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.vault import read_vault_file
|
||||
from ansible.vars import VariableManager
|
||||
|
||||
########################################################
|
||||
|
||||
class AdHocCLI(CLI):
|
||||
''' code behind ansible ad-hoc cli'''
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
|
||||
self.parser = CLI.base_parser(
|
||||
usage='%prog <host-pattern> [options]',
|
||||
runas_opts=True,
|
||||
async_opts=True,
|
||||
output_opts=True,
|
||||
connect_opts=True,
|
||||
check_opts=True,
|
||||
runtask_opts=True,
|
||||
vault_opts=True,
|
||||
)
|
||||
|
||||
# options unique to ansible ad-hoc
|
||||
self.parser.add_option('-a', '--args', dest='module_args',
|
||||
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
|
||||
self.parser.add_option('-m', '--module-name', dest='module_name',
|
||||
help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
|
||||
default=C.DEFAULT_MODULE_NAME)
|
||||
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
|
||||
if len(self.args) != 1:
|
||||
raise AnsibleOptionsError("Missing target hosts")
|
||||
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.validate_conflicts()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def run(self):
|
||||
''' use Runner lib to do SSH things '''
|
||||
|
||||
# only thing left should be host pattern
|
||||
pattern = self.args[0]
|
||||
|
||||
# ignore connection password cause we are local
|
||||
if self.options.connection == "local":
|
||||
self.options.ask_pass = False
|
||||
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vault_pass = None
|
||||
|
||||
self.normalize_become_options()
|
||||
(sshpass, becomepass) = self.ask_passwords()
|
||||
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
vault_pass = self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)[0]
|
||||
|
||||
loader = DataLoader(vault_password=vault_pass)
|
||||
variable_manager = VariableManager()
|
||||
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
|
||||
variable_manager.set_inventory(inventory)
|
||||
|
||||
hosts = inventory.list_hosts(pattern)
|
||||
if len(hosts) == 0:
|
||||
self.display.warning("provided hosts list is empty, only localhost is available")
|
||||
|
||||
if self.options.listhosts:
|
||||
for host in hosts:
|
||||
self.display.display(' %s' % host)
|
||||
return 0
|
||||
|
||||
if self.options.module_name in C.MODULE_REQUIRE_ARGS and not self.options.module_args:
|
||||
raise AnsibleOptionsError("No argument passed to %s module" % self.options.module_name)
|
||||
|
||||
#TODO: implement async support
|
||||
#if self.options.seconds:
|
||||
# callbacks.display("background launch...\n\n", color='cyan')
|
||||
# results, poller = runner.run_async(self.options.seconds)
|
||||
# results = self.poll_while_needed(poller)
|
||||
#else:
|
||||
# results = runner.run()
|
||||
|
||||
# create a pseudo-play to execute the specified module via a single task
|
||||
play_ds = dict(
|
||||
name = "Ansible Ad-Hoc",
|
||||
hosts = pattern,
|
||||
gather_facts = 'no',
|
||||
tasks = [ dict(action=dict(module=self.options.module_name, args=parse_kv(self.options.module_args))), ]
|
||||
)
|
||||
|
||||
play = Play().load(play_ds, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
# now create a task queue manager to execute the play
|
||||
tqm = None
|
||||
try:
|
||||
tqm = TaskQueueManager(
|
||||
inventory=inventory,
|
||||
variable_manager=variable_manager,
|
||||
loader=loader,
|
||||
display=self.display,
|
||||
options=self.options,
|
||||
passwords=passwords,
|
||||
stdout_callback='minimal',
|
||||
)
|
||||
result = tqm.run(play)
|
||||
finally:
|
||||
if tqm:
|
||||
tqm.cleanup()
|
||||
|
||||
return result
|
||||
|
||||
# ----------------------------------------------
|
||||
|
||||
def poll_while_needed(self, poller):
|
||||
''' summarize results from Runner '''
|
||||
|
||||
# BACKGROUND POLL LOGIC when -B and -P are specified
|
||||
if self.options.seconds and self.options.poll_interval > 0:
|
||||
poller.wait(self.options.seconds, self.options.poll_interval)
|
||||
|
||||
return poller.results
|
||||
|
||||
283
lib/ansible/cli/doc.py
Normal file
283
lib/ansible/cli/doc.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# (c) 2014, James Tanner <tanner.jc@gmail.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/>.
|
||||
#
|
||||
# ansible-vault is a script that encrypts/decrypts YAML files. See
|
||||
# http://docs.ansible.com/playbooks_vault.html for more details.
|
||||
|
||||
import fcntl
|
||||
import datetime
|
||||
import os
|
||||
import struct
|
||||
import termios
|
||||
import traceback
|
||||
import textwrap
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.plugins import module_loader
|
||||
from ansible.cli import CLI
|
||||
from ansible.utils import module_docs
|
||||
|
||||
class DocCLI(CLI):
|
||||
""" Vault command line class """
|
||||
|
||||
BLACKLIST_EXTS = ('.pyc', '.swp', '.bak', '~', '.rpm')
|
||||
IGNORE_FILES = [ "COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION"]
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
super(DocCLI, self).__init__(args, display)
|
||||
self.module_list = []
|
||||
|
||||
def parse(self):
|
||||
|
||||
self.parser = CLI.base_parser(
|
||||
usage='usage: %prog [options] [module...]',
|
||||
epilog='Show Ansible module documentation',
|
||||
)
|
||||
|
||||
self.parser.add_option("-M", "--module-path", action="store", dest="module_path", default=C.DEFAULT_MODULE_PATH,
|
||||
help="Ansible modules/ directory")
|
||||
self.parser.add_option("-l", "--list", action="store_true", default=False, dest='list_dir',
|
||||
help='List available modules')
|
||||
self.parser.add_option("-s", "--snippet", action="store_true", default=False, dest='show_snippet',
|
||||
help='Show playbook snippet for specified module(s)')
|
||||
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.options.module_path is not None:
|
||||
for i in self.options.module_path.split(os.pathsep):
|
||||
module_loader.add_directory(i)
|
||||
|
||||
# list modules
|
||||
if self.options.list_dir:
|
||||
paths = module_loader._get_paths()
|
||||
for path in paths:
|
||||
self.find_modules(path)
|
||||
|
||||
CLI.pager(self.get_module_list_text())
|
||||
return 0
|
||||
|
||||
if len(self.args) == 0:
|
||||
raise AnsibleOptionsError("Incorrect options passed")
|
||||
|
||||
# process command line module list
|
||||
text = ''
|
||||
for module in self.args:
|
||||
|
||||
filename = module_loader.find_plugin(module)
|
||||
if filename is None:
|
||||
self.display.warning("module %s not found in %s\n" % (module, DocCLI.print_paths(module_loader)))
|
||||
continue
|
||||
|
||||
if any(filename.endswith(x) for x in self.BLACKLIST_EXTS):
|
||||
continue
|
||||
|
||||
try:
|
||||
doc, plainexamples, returndocs = module_docs.get_docstring(filename)
|
||||
except:
|
||||
self.display.vvv(traceback.print_exc())
|
||||
self.display.error("module %s has a documentation error formatting or is missing documentation\nTo see exact traceback use -vvv" % module)
|
||||
continue
|
||||
|
||||
if doc is not None:
|
||||
|
||||
all_keys = []
|
||||
for (k,v) in doc['options'].iteritems():
|
||||
all_keys.append(k)
|
||||
all_keys = sorted(all_keys)
|
||||
doc['option_keys'] = all_keys
|
||||
|
||||
doc['filename'] = filename
|
||||
doc['docuri'] = doc['module'].replace('_', '-')
|
||||
doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d')
|
||||
doc['plainexamples'] = plainexamples
|
||||
doc['returndocs'] = returndocs
|
||||
|
||||
if self.options.show_snippet:
|
||||
text += DocCLI.get_snippet_text(doc)
|
||||
else:
|
||||
text += DocCLI.get_man_text(doc)
|
||||
else:
|
||||
# this typically means we couldn't even parse the docstring, not just that the YAML is busted,
|
||||
# probably a quoting issue.
|
||||
self.display.warning("module %s missing documentation (or could not parse documentation)\n" % module)
|
||||
|
||||
CLI.pager(text)
|
||||
return 0
|
||||
|
||||
def find_modules(self, path):
|
||||
|
||||
if os.path.isdir(path):
|
||||
for module in os.listdir(path):
|
||||
if module.startswith('.'):
|
||||
continue
|
||||
elif os.path.isdir(module):
|
||||
self.find_modules(module)
|
||||
elif any(module.endswith(x) for x in self.BLACKLIST_EXTS):
|
||||
continue
|
||||
elif module.startswith('__'):
|
||||
continue
|
||||
elif module in self.IGNORE_FILES:
|
||||
continue
|
||||
elif module.startswith('_'):
|
||||
fullpath = '/'.join([path,module])
|
||||
if os.path.islink(fullpath): # avoids aliases
|
||||
continue
|
||||
|
||||
module = os.path.splitext(module)[0] # removes the extension
|
||||
self.module_list.append(module)
|
||||
|
||||
|
||||
def get_module_list_text(self):
|
||||
tty_size = 0
|
||||
if os.isatty(0):
|
||||
tty_size = struct.unpack('HHHH',
|
||||
fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[1]
|
||||
columns = max(60, tty_size)
|
||||
displace = max(len(x) for x in self.module_list)
|
||||
linelimit = columns - displace - 5
|
||||
text = []
|
||||
deprecated = []
|
||||
for module in sorted(set(self.module_list)):
|
||||
|
||||
if module in module_docs.BLACKLIST_MODULES:
|
||||
continue
|
||||
|
||||
filename = module_loader.find_plugin(module)
|
||||
|
||||
if filename is None:
|
||||
continue
|
||||
if filename.endswith(".ps1"):
|
||||
continue
|
||||
if os.path.isdir(filename):
|
||||
continue
|
||||
|
||||
try:
|
||||
doc, plainexamples, returndocs = module_docs.get_docstring(filename)
|
||||
desc = self.tty_ify(doc.get('short_description', '?')).strip()
|
||||
if len(desc) > linelimit:
|
||||
desc = desc[:linelimit] + '...'
|
||||
|
||||
if module.startswith('_'): # Handle deprecated
|
||||
deprecated.append("%-*s %-*.*s" % (displace, module[1:], linelimit, len(desc), desc))
|
||||
else:
|
||||
text.append("%-*s %-*.*s" % (displace, module, linelimit, len(desc), desc))
|
||||
except:
|
||||
raise AnsibleError("module %s has a documentation error formatting or is missing documentation\n" % module)
|
||||
|
||||
if len(deprecated) > 0:
|
||||
text.append("\nDEPRECATED:")
|
||||
text.extend(deprecated)
|
||||
return "\n".join(text)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def print_paths(finder):
|
||||
''' Returns a string suitable for printing of the search path '''
|
||||
|
||||
# Uses a list to get the order right
|
||||
ret = []
|
||||
for i in finder._get_paths():
|
||||
if i not in ret:
|
||||
ret.append(i)
|
||||
return os.pathsep.join(ret)
|
||||
|
||||
@staticmethod
|
||||
def get_snippet_text(doc):
|
||||
|
||||
text = []
|
||||
desc = CLI.tty_ify(" ".join(doc['short_description']))
|
||||
text.append("- name: %s" % (desc))
|
||||
text.append(" action: %s" % (doc['module']))
|
||||
|
||||
for o in sorted(doc['options'].keys()):
|
||||
opt = doc['options'][o]
|
||||
desc = CLI.tty_ify(" ".join(opt['description']))
|
||||
|
||||
if opt.get('required', False):
|
||||
s = o + "="
|
||||
else:
|
||||
s = o
|
||||
|
||||
text.append(" %-20s # %s" % (s, desc))
|
||||
text.append('')
|
||||
|
||||
return "\n".join(text)
|
||||
|
||||
@staticmethod
|
||||
def get_man_text(doc):
|
||||
|
||||
opt_indent=" "
|
||||
text = []
|
||||
text.append("> %s\n" % doc['module'].upper())
|
||||
|
||||
desc = " ".join(doc['description'])
|
||||
|
||||
text.append("%s\n" % textwrap.fill(CLI.tty_ify(desc), initial_indent=" ", subsequent_indent=" "))
|
||||
|
||||
if 'option_keys' in doc and len(doc['option_keys']) > 0:
|
||||
text.append("Options (= is mandatory):\n")
|
||||
|
||||
for o in sorted(doc['option_keys']):
|
||||
opt = doc['options'][o]
|
||||
|
||||
if opt.get('required', False):
|
||||
opt_leadin = "="
|
||||
else:
|
||||
opt_leadin = "-"
|
||||
|
||||
text.append("%s %s" % (opt_leadin, o))
|
||||
|
||||
desc = " ".join(opt['description'])
|
||||
|
||||
if 'choices' in opt:
|
||||
choices = ", ".join(str(i) for i in opt['choices'])
|
||||
desc = desc + " (Choices: " + choices + ")"
|
||||
if 'default' in opt:
|
||||
default = str(opt['default'])
|
||||
desc = desc + " [Default: " + default + "]"
|
||||
text.append("%s\n" % textwrap.fill(CLI.tty_ify(desc), initial_indent=opt_indent,
|
||||
subsequent_indent=opt_indent))
|
||||
|
||||
if 'notes' in doc and len(doc['notes']) > 0:
|
||||
notes = " ".join(doc['notes'])
|
||||
text.append("Notes:%s\n" % textwrap.fill(CLI.tty_ify(notes), initial_indent=" ",
|
||||
subsequent_indent=opt_indent))
|
||||
|
||||
|
||||
if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
|
||||
req = ", ".join(doc['requirements'])
|
||||
text.append("Requirements:%s\n" % textwrap.fill(CLI.tty_ify(req), initial_indent=" ",
|
||||
subsequent_indent=opt_indent))
|
||||
|
||||
if 'examples' in doc and len(doc['examples']) > 0:
|
||||
text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
|
||||
for ex in doc['examples']:
|
||||
text.append("%s\n" % (ex['code']))
|
||||
|
||||
if 'plainexamples' in doc and doc['plainexamples'] is not None:
|
||||
text.append("EXAMPLES:")
|
||||
text.append(doc['plainexamples'])
|
||||
if 'returndocs' in doc and doc['returndocs'] is not None:
|
||||
text.append("RETURN VALUES:")
|
||||
text.append(doc['returndocs'])
|
||||
text.append('')
|
||||
|
||||
return "\n".join(text)
|
||||
491
lib/ansible/cli/galaxy.py
Normal file
491
lib/ansible/cli/galaxy.py
Normal file
@@ -0,0 +1,491 @@
|
||||
########################################################################
|
||||
#
|
||||
# (C) 2013, James Cammarata <jcammarata@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/>.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib
|
||||
import urllib2
|
||||
import yaml
|
||||
|
||||
from collections import defaultdict
|
||||
from distutils.version import LooseVersion
|
||||
from jinja2 import Environment
|
||||
from optparse import OptionParser
|
||||
|
||||
import ansible.constants as C
|
||||
import ansible.utils
|
||||
import ansible.galaxy
|
||||
from ansible.cli import CLI
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.galaxy import Galaxy
|
||||
from ansible.galaxy.api import GalaxyAPI
|
||||
from ansible.galaxy.role import GalaxyRole
|
||||
from ansible.playbook.role.requirement import RoleRequirement
|
||||
from ansible.utils.display import Display
|
||||
|
||||
class GalaxyCLI(CLI):
|
||||
|
||||
VALID_ACTIONS = ("init", "info", "install", "list", "remove")
|
||||
SKIP_INFO_KEYS = ("platforms","readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" )
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
self.api = None
|
||||
self.galaxy = None
|
||||
super(GalaxyCLI, self).__init__(args, display)
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
|
||||
self.parser = CLI.base_parser(
|
||||
usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS),
|
||||
epilog = "\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
|
||||
)
|
||||
|
||||
|
||||
self.set_action()
|
||||
|
||||
# options specific to actions
|
||||
if self.action == "info":
|
||||
self.parser.set_usage("usage: %prog info [options] role_name[,version]")
|
||||
elif self.action == "init":
|
||||
self.parser.set_usage("usage: %prog init [options] role_name")
|
||||
self.parser.add_option(
|
||||
'-p', '--init-path', dest='init_path', default="./",
|
||||
help='The path in which the skeleton role will be created. '
|
||||
'The default is the current working directory.')
|
||||
self.parser.add_option(
|
||||
'--offline', dest='offline', default=False, action='store_true',
|
||||
help="Don't query the galaxy API when creating roles")
|
||||
elif self.action == "install":
|
||||
self.parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]")
|
||||
self.parser.add_option(
|
||||
'-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
|
||||
help='Ignore errors and continue with the next specified role.')
|
||||
self.parser.add_option(
|
||||
'-n', '--no-deps', dest='no_deps', action='store_true', default=False,
|
||||
help='Don\'t download roles listed as dependencies')
|
||||
self.parser.add_option(
|
||||
'-r', '--role-file', dest='role_file',
|
||||
help='A file containing a list of roles to be imported')
|
||||
elif self.action == "remove":
|
||||
self.parser.set_usage("usage: %prog remove role1 role2 ...")
|
||||
elif self.action == "list":
|
||||
self.parser.set_usage("usage: %prog list [role_name]")
|
||||
|
||||
# options that apply to more than one action
|
||||
if self.action != "init":
|
||||
self.parser.add_option(
|
||||
'-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH,
|
||||
help='The path to the directory containing your roles. '
|
||||
'The default is the roles_path configured in your '
|
||||
'ansible.cfg file (/etc/ansible/roles if not configured)')
|
||||
|
||||
if self.action in ("info","init","install"):
|
||||
self.parser.add_option( '-s', '--server', dest='api_server', default="https://galaxy.ansible.com",
|
||||
help='The API server destination')
|
||||
|
||||
if self.action in ("init","install"):
|
||||
self.parser.add_option(
|
||||
'-f', '--force', dest='force', action='store_true', default=False,
|
||||
help='Force overwriting an existing role')
|
||||
|
||||
# get options, args and galaxy object
|
||||
self.options, self.args =self.parser.parse_args()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.galaxy = Galaxy(self.options, self.display)
|
||||
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
|
||||
# if not offline, get connect to galaxy api
|
||||
if self.action in ("info","install") or (self.action == 'init' and not self.options.offline):
|
||||
api_server = self.options.api_server
|
||||
self.api = GalaxyAPI(self.galaxy, api_server)
|
||||
if not self.api:
|
||||
raise AnsibleError("The API server (%s) is not responding, please try again later." % api_server)
|
||||
|
||||
self.execute()
|
||||
|
||||
def get_opt(self, k, defval=""):
|
||||
"""
|
||||
Returns an option from an Optparse values instance.
|
||||
"""
|
||||
try:
|
||||
data = getattr(self.options, k)
|
||||
except:
|
||||
return defval
|
||||
if k == "roles_path":
|
||||
if os.pathsep in data:
|
||||
data = data.split(os.pathsep)[0]
|
||||
return data
|
||||
|
||||
def exit_without_ignore(self, rc=1):
|
||||
"""
|
||||
Exits with the specified return code unless the
|
||||
option --ignore-errors was specified
|
||||
"""
|
||||
if not self.get_opt("ignore_errors", False):
|
||||
self.display.error('- you can use --ignore-errors to skip failed roles and finish processing the list.')
|
||||
return rc
|
||||
|
||||
def execute_init(self):
|
||||
"""
|
||||
Executes the init action, which creates the skeleton framework
|
||||
of a role that complies with the galaxy metadata format.
|
||||
"""
|
||||
|
||||
init_path = self.get_opt('init_path', './')
|
||||
force = self.get_opt('force', False)
|
||||
offline = self.get_opt('offline', False)
|
||||
|
||||
role_name = self.args.pop(0).strip()
|
||||
if role_name == "":
|
||||
raise AnsibleOptionsError("- no role name specified for init")
|
||||
role_path = os.path.join(init_path, role_name)
|
||||
if os.path.exists(role_path):
|
||||
if os.path.isfile(role_path):
|
||||
raise AnsibleError("- the path %s already exists, but is a file - aborting" % role_path)
|
||||
elif not force:
|
||||
raise AnsibleError("- the directory %s already exists." % role_path + \
|
||||
"you can use --force to re-initialize this directory,\n" + \
|
||||
"however it will reset any main.yml files that may have\n" + \
|
||||
"been modified there already.")
|
||||
|
||||
# create the default README.md
|
||||
if not os.path.exists(role_path):
|
||||
os.makedirs(role_path)
|
||||
readme_path = os.path.join(role_path, "README.md")
|
||||
f = open(readme_path, "wb")
|
||||
f.write(self.galaxy.default_readme)
|
||||
f.close
|
||||
|
||||
for dir in GalaxyRole.ROLE_DIRS:
|
||||
dir_path = os.path.join(init_path, role_name, dir)
|
||||
main_yml_path = os.path.join(dir_path, 'main.yml')
|
||||
# create the directory if it doesn't exist already
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
# now create the main.yml file for that directory
|
||||
if dir == "meta":
|
||||
# create a skeleton meta/main.yml with a valid galaxy_info
|
||||
# datastructure in place, plus with all of the available
|
||||
# tags/platforms included (but commented out) and the
|
||||
# dependencies section
|
||||
platforms = []
|
||||
if not offline and self.api:
|
||||
platforms = self.api.get_list("platforms") or []
|
||||
categories = []
|
||||
if not offline and self.api:
|
||||
categories = self.api.get_list("categories") or []
|
||||
|
||||
# group the list of platforms from the api based
|
||||
# on their names, with the release field being
|
||||
# appended to a list of versions
|
||||
platform_groups = defaultdict(list)
|
||||
for platform in platforms:
|
||||
platform_groups[platform['name']].append(platform['release'])
|
||||
platform_groups[platform['name']].sort()
|
||||
|
||||
inject = dict(
|
||||
author = 'your name',
|
||||
company = 'your company (optional)',
|
||||
license = 'license (GPLv2, CC-BY, etc)',
|
||||
issue_tracker_url = 'http://example.com/issue/tracker',
|
||||
min_ansible_version = '1.2',
|
||||
platforms = platform_groups,
|
||||
categories = categories,
|
||||
)
|
||||
rendered_meta = Environment().from_string(self.galaxy.default_meta).render(inject)
|
||||
f = open(main_yml_path, 'w')
|
||||
f.write(rendered_meta)
|
||||
f.close()
|
||||
pass
|
||||
elif dir not in ('files','templates'):
|
||||
# just write a (mostly) empty YAML file for main.yml
|
||||
f = open(main_yml_path, 'w')
|
||||
f.write('---\n# %s file for %s\n' % (dir,role_name))
|
||||
f.close()
|
||||
self.display.display("- %s was created successfully" % role_name)
|
||||
|
||||
def execute_info(self):
|
||||
"""
|
||||
Executes the info action. This action prints out detailed
|
||||
information about an installed role as well as info available
|
||||
from the galaxy API.
|
||||
"""
|
||||
|
||||
if len(self.args) == 0:
|
||||
# the user needs to specify a role
|
||||
raise AnsibleOptionsError("- you must specify a user/role name")
|
||||
|
||||
roles_path = self.get_opt("roles_path")
|
||||
|
||||
for role in self.args:
|
||||
|
||||
role_info = {}
|
||||
gr = GalaxyRole(self.galaxy, role)
|
||||
#self.galaxy.add_role(gr)
|
||||
|
||||
install_info = gr.install_info
|
||||
if install_info:
|
||||
if 'version' in install_info:
|
||||
install_info['intalled_version'] = install_info['version']
|
||||
del install_info['version']
|
||||
role_info.update(install_info)
|
||||
|
||||
remote_data = False
|
||||
if self.api:
|
||||
remote_data = self.api.lookup_role_by_name(role, False)
|
||||
|
||||
if remote_data:
|
||||
role_info.update(remote_data)
|
||||
|
||||
if gr.metadata:
|
||||
role_info.update(gr.metadata)
|
||||
|
||||
req = RoleRequirement()
|
||||
__, __, role_spec= req.parse({'role': role})
|
||||
if role_spec:
|
||||
role_info.update(role_spec)
|
||||
|
||||
if role_info:
|
||||
self.display.display("- %s:" % (role))
|
||||
for k in sorted(role_info.keys()):
|
||||
|
||||
if k in self.SKIP_INFO_KEYS:
|
||||
continue
|
||||
|
||||
if isinstance(role_info[k], dict):
|
||||
self.display.display("\t%s: " % (k))
|
||||
for key in sorted(role_info[k].keys()):
|
||||
if key in self.SKIP_INFO_KEYS:
|
||||
continue
|
||||
self.display.display("\t\t%s: %s" % (key, role_info[k][key]))
|
||||
else:
|
||||
self.display.display("\t%s: %s" % (k, role_info[k]))
|
||||
else:
|
||||
self.display.display("- the role %s was not found" % role)
|
||||
|
||||
def execute_install(self):
|
||||
"""
|
||||
Executes the installation action. The args list contains the
|
||||
roles to be installed, unless -f was specified. The list of roles
|
||||
can be a name (which will be downloaded via the galaxy API and github),
|
||||
or it can be a local .tar.gz file.
|
||||
"""
|
||||
|
||||
role_file = self.get_opt("role_file", None)
|
||||
|
||||
if len(self.args) == 0 and role_file is None:
|
||||
# the user needs to specify one of either --role-file
|
||||
# or specify a single user/role name
|
||||
raise AnsibleOptionsError("- you must specify a user/role name or a roles file")
|
||||
elif len(self.args) == 1 and not role_file is None:
|
||||
# using a role file is mutually exclusive of specifying
|
||||
# the role name on the command line
|
||||
raise AnsibleOptionsError("- please specify a user/role name, or a roles file, but not both")
|
||||
|
||||
no_deps = self.get_opt("no_deps", False)
|
||||
roles_path = self.get_opt("roles_path")
|
||||
|
||||
roles_done = []
|
||||
roles_left = []
|
||||
role_name = self.args.pop(0).strip()
|
||||
|
||||
gr = GalaxyRole(self.galaxy, role_name)
|
||||
if role_file:
|
||||
f = open(role_file, 'r')
|
||||
if role_file.endswith('.yaml') or role_file.endswith('.yml'):
|
||||
roles_left = map(ansible.utils.role_yaml_parse, yaml.safe_load(f))
|
||||
else:
|
||||
# roles listed in a file, one per line
|
||||
for rname in f.readlines():
|
||||
roles_left.append(GalaxyRole(self.galaxy, rname))
|
||||
f.close()
|
||||
else:
|
||||
# roles were specified directly, so we'll just go out grab them
|
||||
# (and their dependencies, unless the user doesn't want us to).
|
||||
for rname in self.args:
|
||||
roles_left.append(GalaxyRole(self.galaxy, rname))
|
||||
|
||||
while len(roles_left) > 0:
|
||||
# query the galaxy API for the role data
|
||||
role_data = None
|
||||
role = roles_left.pop(0)
|
||||
role_src = role.src
|
||||
role_scm = role.scm
|
||||
role_path = role.path
|
||||
|
||||
if role_path:
|
||||
self.options.roles_path = role_path
|
||||
else:
|
||||
self.options.roles_path = roles_path
|
||||
|
||||
tmp_file = None
|
||||
if role_src and os.path.isfile(role_src):
|
||||
# installing a local tar.gz
|
||||
tmp_file = role_src
|
||||
else:
|
||||
if role_scm:
|
||||
# create tar file from scm url
|
||||
tmp_file = scm_archive_role(role_scm, role_src, role.version, role.name)
|
||||
if role_src:
|
||||
if '://' in role_src:
|
||||
# just download a URL - version will probably be in the URL
|
||||
tmp_file = gr.fetch()
|
||||
else:
|
||||
role_data = self.api.lookup_role_by_name(role_src)
|
||||
if not role_data:
|
||||
self.display.warning("- sorry, %s was not found on %s." % (role_src, self.options.api_server))
|
||||
self.exit_without_ignore()
|
||||
continue
|
||||
|
||||
role_versions = self.api.fetch_role_related('versions', role_data['id'])
|
||||
if not role.version:
|
||||
# convert the version names to LooseVersion objects
|
||||
# and sort them to get the latest version. If there
|
||||
# are no versions in the list, we'll grab the head
|
||||
# of the master branch
|
||||
if len(role_versions) > 0:
|
||||
loose_versions = [LooseVersion(a.get('name',None)) for a in role_versions]
|
||||
loose_versions.sort()
|
||||
role["version"] = str(loose_versions[-1])
|
||||
else:
|
||||
role["version"] = 'master'
|
||||
elif role['version'] != 'master':
|
||||
if role_versions and role.version not in [a.get('name', None) for a in role_versions]:
|
||||
self.display.warning('role is %s' % role)
|
||||
self.display.warning("- the specified version (%s) was not found in the list of available versions (%s)." % (role.version, role_versions))
|
||||
self.exit_without_ignore()
|
||||
continue
|
||||
|
||||
# download the role. if --no-deps was specified, we stop here,
|
||||
# otherwise we recursively grab roles and all of their deps.
|
||||
tmp_file = gr.fetch(role_data)
|
||||
installed = False
|
||||
if tmp_file:
|
||||
installed = install_role(role.name, role.version, tmp_file, options)
|
||||
# we're done with the temp file, clean it up
|
||||
if tmp_file != role_src:
|
||||
os.unlink(tmp_file)
|
||||
# install dependencies, if we want them
|
||||
|
||||
# this should use new roledepenencies code
|
||||
#if not no_deps and installed:
|
||||
# if not role_data:
|
||||
# role_data = gr.get_metadata(role.get("name"), options)
|
||||
# role_dependencies = role_data['dependencies']
|
||||
# else:
|
||||
# role_dependencies = role_data['summary_fields']['dependencies'] # api_fetch_role_related(api_server, 'dependencies', role_data['id'])
|
||||
# for dep in role_dependencies:
|
||||
# if isinstance(dep, basestring):
|
||||
# dep = ansible.utils.role_spec_parse(dep)
|
||||
# else:
|
||||
# dep = ansible.utils.role_yaml_parse(dep)
|
||||
# if not get_role_metadata(dep["name"], options):
|
||||
# if dep not in roles_left:
|
||||
# print '- adding dependency: %s' % dep["name"]
|
||||
# roles_left.append(dep)
|
||||
# else:
|
||||
# print '- dependency %s already pending installation.' % dep["name"]
|
||||
# else:
|
||||
# print '- dependency %s is already installed, skipping.' % dep["name"]
|
||||
|
||||
if not tmp_file or not installed:
|
||||
self.display.warning("- %s was NOT installed successfully." % role.name)
|
||||
self.exit_without_ignore()
|
||||
return 0
|
||||
|
||||
def execute_remove(self):
|
||||
"""
|
||||
Executes the remove action. The args list contains the list
|
||||
of roles to be removed. This list can contain more than one role.
|
||||
"""
|
||||
|
||||
if len(self.args) == 0:
|
||||
raise AnsibleOptionsError('- you must specify at least one role to remove.')
|
||||
|
||||
for role_name in self.args:
|
||||
role = GalaxyRole(self.galaxy, role_name)
|
||||
try:
|
||||
if role.remove():
|
||||
self.display.display('- successfully removed %s' % role_name)
|
||||
else:
|
||||
self.display.display('- %s is not installed, skipping.' % role_name)
|
||||
except Exception as e:
|
||||
raise AnsibleError("Failed to remove role %s: %s" % (role_name, str(e)))
|
||||
|
||||
return 0
|
||||
|
||||
def execute_list(self):
|
||||
"""
|
||||
Executes the list action. The args list can contain zero
|
||||
or one role. If one is specified, only that role will be
|
||||
shown, otherwise all roles in the specified directory will
|
||||
be shown.
|
||||
"""
|
||||
|
||||
if len(self.args) > 1:
|
||||
raise AnsibleOptionsError("- please specify only one role to list, or specify no roles to see a full list")
|
||||
|
||||
if len(self.args) == 1:
|
||||
# show only the request role, if it exists
|
||||
gr = GalaxyRole(self.galaxy, self.name)
|
||||
if gr.metadata:
|
||||
install_info = gr.install_info
|
||||
version = None
|
||||
if install_info:
|
||||
version = install_info.get("version", None)
|
||||
if not version:
|
||||
version = "(unknown version)"
|
||||
# show some more info about single roles here
|
||||
self.display.display("- %s, %s" % (self.name, version))
|
||||
else:
|
||||
self.display.display("- the role %s was not found" % self.name)
|
||||
else:
|
||||
# show all valid roles in the roles_path directory
|
||||
roles_path = self.get_opt('roles_path')
|
||||
roles_path = os.path.expanduser(roles_path)
|
||||
if not os.path.exists(roles_path):
|
||||
raise AnsibleOptionsError("- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path)
|
||||
elif not os.path.isdir(roles_path):
|
||||
raise AnsibleOptionsError("- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path)
|
||||
path_files = os.listdir(roles_path)
|
||||
for path_file in path_files:
|
||||
if gr.metadata:
|
||||
install_info = gr.metadata
|
||||
version = None
|
||||
if install_info:
|
||||
version = install_info.get("version", None)
|
||||
if not version:
|
||||
version = "(unknown version)"
|
||||
self.display.display("- %s, %s" % (path_file, version))
|
||||
return 0
|
||||
180
lib/ansible/cli/playbook.py
Normal file
180
lib/ansible/cli/playbook.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# (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/>.
|
||||
|
||||
########################################################
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.cli import CLI
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.executor.playbook_executor import PlaybookExecutor
|
||||
from ansible.inventory import Inventory
|
||||
from ansible.parsing import DataLoader
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.playbook import Playbook
|
||||
from ansible.playbook.task import Task
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.unicode import to_unicode
|
||||
from ansible.utils.vars import combine_vars
|
||||
from ansible.utils.vault import read_vault_file
|
||||
from ansible.vars import VariableManager
|
||||
|
||||
#---------------------------------------------------------------------------------------------------
|
||||
|
||||
class PlaybookCLI(CLI):
|
||||
''' code behind ansible playbook cli'''
|
||||
|
||||
def parse(self):
|
||||
|
||||
# create parser for CLI options
|
||||
parser = CLI.base_parser(
|
||||
usage = "%prog playbook.yml",
|
||||
connect_opts=True,
|
||||
meta_opts=True,
|
||||
runas_opts=True,
|
||||
subset_opts=True,
|
||||
check_opts=True,
|
||||
diff_opts=True,
|
||||
runtask_opts=True,
|
||||
vault_opts=True,
|
||||
)
|
||||
|
||||
# ansible playbook specific opts
|
||||
parser.add_option('--list-tasks', dest='listtasks', action='store_true',
|
||||
help="list all tasks that would be executed")
|
||||
parser.add_option('--step', dest='step', action='store_true',
|
||||
help="one-step-at-a-time: confirm each task before running")
|
||||
parser.add_option('--start-at-task', dest='start_at',
|
||||
help="start the playbook at the task matching this name")
|
||||
parser.add_option('--list-tags', dest='listtags', action='store_true',
|
||||
help="list all available tags")
|
||||
|
||||
self.options, self.args = parser.parse_args()
|
||||
|
||||
if len(self.args) == 0:
|
||||
raise AnsibleOptionsError("You must specify a playbook file to run")
|
||||
|
||||
self.parser = parser
|
||||
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.validate_conflicts()
|
||||
|
||||
def run(self):
|
||||
|
||||
# Note: slightly wrong, this is written so that implicit localhost
|
||||
# Manage passwords
|
||||
sshpass = None
|
||||
becomepass = None
|
||||
vault_pass = None
|
||||
passwords = {}
|
||||
|
||||
# don't deal with privilege escalation or passwords when we don't need to
|
||||
if not self.options.listhosts and not self.options.listtasks and not self.options.listtags and not self.options.syntax:
|
||||
self.normalize_become_options()
|
||||
(sshpass, becomepass) = self.ask_passwords()
|
||||
passwords = { 'conn_pass': sshpass, 'become_pass': becomepass }
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
vault_pass = self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)[0]
|
||||
|
||||
loader = DataLoader(vault_password=vault_pass)
|
||||
|
||||
extra_vars = {}
|
||||
for extra_vars_opt in self.options.extra_vars:
|
||||
extra_vars_opt = to_unicode(extra_vars_opt, errors='strict')
|
||||
if extra_vars_opt.startswith(u"@"):
|
||||
# Argument is a YAML file (JSON is a subset of YAML)
|
||||
data = loader.load_from_file(extra_vars_opt[1:])
|
||||
elif extra_vars_opt and extra_vars_opt[0] in u'[{':
|
||||
# Arguments as YAML
|
||||
data = loader.load(extra_vars_opt)
|
||||
else:
|
||||
# Arguments as Key-value
|
||||
data = parse_kv(extra_vars_opt)
|
||||
extra_vars = combine_vars(extra_vars, data)
|
||||
|
||||
# FIXME: this should be moved inside the playbook executor code
|
||||
only_tags = self.options.tags.split(",")
|
||||
skip_tags = self.options.skip_tags
|
||||
if self.options.skip_tags is not None:
|
||||
skip_tags = self.options.skip_tags.split(",")
|
||||
|
||||
# initial error check, to make sure all specified playbooks are accessible
|
||||
# before we start running anything through the playbook executor
|
||||
for playbook in self.args:
|
||||
if not os.path.exists(playbook):
|
||||
raise AnsibleError("the playbook: %s could not be found" % playbook)
|
||||
if not (os.path.isfile(playbook) or stat.S_ISFIFO(os.stat(playbook).st_mode)):
|
||||
raise AnsibleError("the playbook: %s does not appear to be a file" % playbook)
|
||||
|
||||
# create the variable manager, which will be shared throughout
|
||||
# the code, ensuring a consistent view of global variables
|
||||
variable_manager = VariableManager()
|
||||
variable_manager.extra_vars = extra_vars
|
||||
|
||||
# create the inventory, and filter it based on the subset specified (if any)
|
||||
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=self.options.inventory)
|
||||
variable_manager.set_inventory(inventory)
|
||||
|
||||
# (which is not returned in list_hosts()) is taken into account for
|
||||
# warning if inventory is empty. But it can't be taken into account for
|
||||
# checking if limit doesn't match any hosts. Instead we don't worry about
|
||||
# limit if only implicit localhost was in inventory to start with.
|
||||
#
|
||||
# Fix this when we rewrite inventory by making localhost a real host (and thus show up in list_hosts())
|
||||
no_hosts = False
|
||||
if len(inventory.list_hosts()) == 0:
|
||||
# Empty inventory
|
||||
self.display.warning("provided hosts list is empty, only localhost is available")
|
||||
no_hosts = True
|
||||
inventory.subset(self.options.subset)
|
||||
if len(inventory.list_hosts()) == 0 and no_hosts is False:
|
||||
# Invalid limit
|
||||
raise AnsibleError("Specified --limit does not match any hosts")
|
||||
|
||||
# create the playbook executor, which manages running the plays via a task queue manager
|
||||
pbex = PlaybookExecutor(playbooks=self.args, inventory=inventory, variable_manager=variable_manager, loader=loader, display=self.display, options=self.options, passwords=passwords)
|
||||
|
||||
results = pbex.run()
|
||||
|
||||
if isinstance(results, list):
|
||||
for p in results:
|
||||
|
||||
self.display.display('\nplaybook: %s\n' % p['playbook'])
|
||||
for play in p['plays']:
|
||||
if self.options.listhosts:
|
||||
self.display.display("\n %s (%s): host count=%d" % (play['name'], play['pattern'], len(play['hosts'])))
|
||||
for host in play['hosts']:
|
||||
self.display.display(" %s" % host)
|
||||
if self.options.listtasks: #TODO: do we want to display block info?
|
||||
self.display.display("\n %s" % (play['name']))
|
||||
for task in play['tasks']:
|
||||
self.display.display(" %s" % task)
|
||||
if self.options.listtags: #TODO: fix once we figure out block handling above
|
||||
self.display.display("\n %s: tags count=%d" % (play['name'], len(play['tags'])))
|
||||
for tag in play['tags']:
|
||||
self.display.display(" %s" % tag)
|
||||
return 0
|
||||
else:
|
||||
return results
|
||||
219
lib/ansible/cli/pull.py
Normal file
219
lib/ansible/cli/pull.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# (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/>.
|
||||
|
||||
########################################################
|
||||
import datetime
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import socket
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.vault import read_vault_file
|
||||
|
||||
########################################################
|
||||
|
||||
class PullCLI(CLI):
|
||||
''' code behind ansible ad-hoc cli'''
|
||||
|
||||
DEFAULT_REPO_TYPE = 'git'
|
||||
DEFAULT_PLAYBOOK = 'local.yml'
|
||||
PLAYBOOK_ERRORS = {
|
||||
1: 'File does not exist',
|
||||
2: 'File is not readable'
|
||||
}
|
||||
SUPPORTED_REPO_MODULES = ['git']
|
||||
|
||||
def parse(self):
|
||||
''' create an options parser for bin/ansible '''
|
||||
|
||||
self.parser = CLI.base_parser(
|
||||
usage='%prog <host-pattern> [options]',
|
||||
connect_opts=True,
|
||||
vault_opts=True,
|
||||
)
|
||||
|
||||
# options unique to pull
|
||||
self.parser.add_option('--purge', default=False, action='store_true', help='purge checkout after playbook run')
|
||||
self.parser.add_option('-o', '--only-if-changed', dest='ifchanged', default=False, action='store_true',
|
||||
help='only run the playbook if the repository has been updated')
|
||||
self.parser.add_option('-s', '--sleep', dest='sleep', default=None,
|
||||
help='sleep for random interval (between 0 and n number of seconds) before starting. This is a useful way to disperse git requests')
|
||||
self.parser.add_option('-f', '--force', dest='force', default=False, action='store_true',
|
||||
help='run the playbook even if the repository could not be updated')
|
||||
self.parser.add_option('-d', '--directory', dest='dest', default=None, help='directory to checkout repository to')
|
||||
self.parser.add_option('-U', '--url', dest='url', default=None, help='URL of the playbook repository')
|
||||
self.parser.add_option('-C', '--checkout', dest='checkout',
|
||||
help='branch/tag/commit to checkout. ' 'Defaults to behavior of repository module.')
|
||||
self.parser.add_option('--accept-host-key', default=False, dest='accept_host_key', action='store_true',
|
||||
help='adds the hostkey for the repo url if not already added')
|
||||
self.parser.add_option('-m', '--module-name', dest='module_name', default=self.DEFAULT_REPO_TYPE,
|
||||
help='Repository module name, which ansible will use to check out the repo. Default is %s.' % self.DEFAULT_REPO_TYPE)
|
||||
|
||||
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
|
||||
if self.options.sleep:
|
||||
try:
|
||||
secs = random.randint(0,int(self.options.sleep))
|
||||
self.options.sleep = secs
|
||||
except ValueError:
|
||||
raise AnsibleOptionsError("%s is not a number." % self.options.sleep)
|
||||
|
||||
if not self.options.url:
|
||||
raise AnsibleOptionsError("URL for repository not specified, use -h for help")
|
||||
|
||||
if len(self.args) != 1:
|
||||
raise AnsibleOptionsError("Missing target hosts")
|
||||
|
||||
if self.options.module_name not in self.SUPPORTED_REPO_MODULES:
|
||||
raise AnsibleOptionsError("Unsuported repo module %s, choices are %s" % (self.options.module_name, ','.join(self.SUPPORTED_REPO_MODULES)))
|
||||
|
||||
self.display.verbosity = self.options.verbosity
|
||||
self.validate_conflicts()
|
||||
|
||||
def run(self):
|
||||
''' use Runner lib to do SSH things '''
|
||||
|
||||
# log command line
|
||||
now = datetime.datetime.now()
|
||||
self.display.display(now.strftime("Starting Ansible Pull at %F %T"))
|
||||
self.display.display(' '.join(sys.argv))
|
||||
|
||||
# Build Checkout command
|
||||
# Now construct the ansible command
|
||||
limit_opts = 'localhost:%s:127.0.0.1' % socket.getfqdn()
|
||||
base_opts = '-c local --limit "%s"' % limit_opts
|
||||
if self.options.verbosity > 0:
|
||||
base_opts += ' -%s' % ''.join([ "v" for x in range(0, self.options.verbosity) ])
|
||||
|
||||
# Attempt to use the inventory passed in as an argument
|
||||
# It might not yet have been downloaded so use localhost if note
|
||||
if not self.options.inventory or not os.path.exists(self.options.inventory):
|
||||
inv_opts = 'localhost,'
|
||||
else:
|
||||
inv_opts = self.options.inventory
|
||||
|
||||
#TODO: enable more repo modules hg/svn?
|
||||
if self.options.module_name == 'git':
|
||||
repo_opts = "name=%s dest=%s" % (self.options.url, self.options.dest)
|
||||
if self.options.checkout:
|
||||
repo_opts += ' version=%s' % self.options.checkout
|
||||
|
||||
if self.options.accept_host_key:
|
||||
repo_opts += ' accept_hostkey=yes'
|
||||
|
||||
if self.options.key_file:
|
||||
repo_opts += ' key_file=%s' % options.key_file
|
||||
|
||||
path = utils.plugins.module_finder.find_plugin(options.module_name)
|
||||
if path is None:
|
||||
raise AnsibleOptionsError(("module '%s' not found.\n" % options.module_name))
|
||||
|
||||
bin_path = os.path.dirname(os.path.abspath(__file__))
|
||||
cmd = '%s/ansible localhost -i "%s" %s -m %s -a "%s"' % (
|
||||
bin_path, inv_opts, base_opts, self.options.module_name, repo_opts
|
||||
)
|
||||
|
||||
for ev in self.options.extra_vars:
|
||||
cmd += ' -e "%s"' % ev
|
||||
|
||||
# Nap?
|
||||
if self.options.sleep:
|
||||
self.display.display("Sleeping for %d seconds..." % self.options.sleep)
|
||||
time.sleep(self.options.sleep);
|
||||
|
||||
# RUN the Checkout command
|
||||
rc, out, err = cmd_functions.run_cmd(cmd, live=True)
|
||||
|
||||
if rc != 0:
|
||||
if self.options.force:
|
||||
self.display.warning("Unable to update repository. Continuing with (forced) run of playbook.")
|
||||
else:
|
||||
return rc
|
||||
elif self.options.ifchanged and '"changed": true' not in out:
|
||||
self.display.display("Repository has not changed, quitting.")
|
||||
return 0
|
||||
|
||||
playbook = self.select_playbook(path)
|
||||
|
||||
if playbook is None:
|
||||
raise AnsibleOptionsError("Could not find a playbook to run.")
|
||||
|
||||
# Build playbook command
|
||||
cmd = '%s/ansible-playbook %s %s' % (bin_path, base_opts, playbook)
|
||||
if self.options.vault_password_file:
|
||||
cmd += " --vault-password-file=%s" % self.options.vault_password_file
|
||||
if self.options.inventory:
|
||||
cmd += ' -i "%s"' % self.options.inventory
|
||||
for ev in self.options.extra_vars:
|
||||
cmd += ' -e "%s"' % ev
|
||||
if self.options.ask_sudo_pass:
|
||||
cmd += ' -K'
|
||||
if self.options.tags:
|
||||
cmd += ' -t "%s"' % self.options.tags
|
||||
|
||||
os.chdir(self.options.dest)
|
||||
|
||||
# RUN THE PLAYBOOK COMMAND
|
||||
rc, out, err = cmd_functions.run_cmd(cmd, live=True)
|
||||
|
||||
if self.options.purge:
|
||||
os.chdir('/')
|
||||
try:
|
||||
shutil.rmtree(options.dest)
|
||||
except Exception, e:
|
||||
print >>sys.stderr, "Failed to remove %s: %s" % (options.dest, str(e))
|
||||
|
||||
return rc
|
||||
|
||||
|
||||
def try_playbook(self, path):
|
||||
if not os.path.exists(path):
|
||||
return 1
|
||||
if not os.access(path, os.R_OK):
|
||||
return 2
|
||||
return 0
|
||||
|
||||
def select_playbook(self, path):
|
||||
playbook = None
|
||||
if len(self.args) > 0 and self.args[0] is not None:
|
||||
playbook = os.path.join(path, self.args[0])
|
||||
rc = self.try_playbook(playbook)
|
||||
if rc != 0:
|
||||
self.display.warning("%s: %s" % (playbook, self.PLAYBOOK_ERRORS[rc]))
|
||||
return None
|
||||
return playbook
|
||||
else:
|
||||
fqdn = socket.getfqdn()
|
||||
hostpb = os.path.join(path, fqdn + '.yml')
|
||||
shorthostpb = os.path.join(path, fqdn.split('.')[0] + '.yml')
|
||||
localpb = os.path.join(path, DEFAULT_PLAYBOOK)
|
||||
errors = []
|
||||
for pb in [hostpb, shorthostpb, localpb]:
|
||||
rc = self.try_playbook(pb)
|
||||
if rc == 0:
|
||||
playbook = pb
|
||||
break
|
||||
else:
|
||||
errors.append("%s: %s" % (pb, self.PLAYBOOK_ERRORS[rc]))
|
||||
if playbook is None:
|
||||
self.display.warning("\n".join(errors))
|
||||
return playbook
|
||||
123
lib/ansible/cli/vault.py
Normal file
123
lib/ansible/cli/vault.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# (c) 2014, James Tanner <tanner.jc@gmail.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/>.
|
||||
#
|
||||
# ansible-vault is a script that encrypts/decrypts YAML files. See
|
||||
# http://docs.ansible.com/playbooks_vault.html for more details.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError
|
||||
from ansible.parsing.vault import VaultEditor
|
||||
from ansible.cli import CLI
|
||||
from ansible.utils.display import Display
|
||||
|
||||
class VaultCLI(CLI):
|
||||
""" Vault command line class """
|
||||
|
||||
VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view")
|
||||
CIPHER = 'AES256'
|
||||
|
||||
def __init__(self, args, display=None):
|
||||
|
||||
self.vault_pass = None
|
||||
super(VaultCLI, self).__init__(args, display)
|
||||
|
||||
def parse(self):
|
||||
|
||||
self.parser = CLI.base_parser(
|
||||
vault_opts=True,
|
||||
usage = "usage: %%prog [%s] [--help] [options] vaultfile.yml" % "|".join(self.VALID_ACTIONS),
|
||||
epilog = "\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
|
||||
)
|
||||
|
||||
self.set_action()
|
||||
|
||||
# options specific to self.actions
|
||||
if self.action == "create":
|
||||
self.parser.set_usage("usage: %prog create [options] file_name")
|
||||
elif self.action == "decrypt":
|
||||
self.parser.set_usage("usage: %prog decrypt [options] file_name")
|
||||
elif self.action == "edit":
|
||||
self.parser.set_usage("usage: %prog edit [options] file_name")
|
||||
elif self.action == "view":
|
||||
self.parser.set_usage("usage: %prog view [options] file_name")
|
||||
elif self.action == "encrypt":
|
||||
self.parser.set_usage("usage: %prog encrypt [options] file_name")
|
||||
elif action == "rekey":
|
||||
self.parser.set_usage("usage: %prog rekey [options] file_name")
|
||||
|
||||
self.options, self.args = self.parser.parse_args()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
|
||||
if len(self.args) == 0 or len(self.args) > 1:
|
||||
raise AnsibleOptionsError("Vault requires a single filename as a parameter")
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.options.vault_password_file:
|
||||
# read vault_pass from a file
|
||||
self.vault_pass = read_vault_file(self.options.vault_password_file)
|
||||
elif self.options.ask_vault_pass:
|
||||
self.vault_pass, _= self.ask_vault_passwords(ask_vault_pass=True, ask_new_vault_pass=False, confirm_new=False)
|
||||
|
||||
self.execute()
|
||||
|
||||
def execute_create(self):
|
||||
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, self.args[0])
|
||||
this_editor.create_file()
|
||||
|
||||
def execute_decrypt(self):
|
||||
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
this_editor.decrypt_file()
|
||||
|
||||
self.display.display("Decryption successful")
|
||||
|
||||
def execute_edit(self):
|
||||
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.edit_file()
|
||||
|
||||
def execute_view(self):
|
||||
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.view_file()
|
||||
|
||||
def execute_encrypt(self):
|
||||
|
||||
cipher = getattr(self.options, 'cipher', self.CIPHER)
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(cipher, self.vault_pass, f)
|
||||
this_editor.encrypt_file()
|
||||
|
||||
self.display.display("Encryption successful")
|
||||
|
||||
def execute_rekey(self):
|
||||
__, new_password = self.ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True)
|
||||
|
||||
for f in self.args:
|
||||
this_editor = VaultEditor(None, self.vault_pass, f)
|
||||
this_editor.rekey_file(new_password)
|
||||
|
||||
self.display.display("Rekey successful")
|
||||
27
lib/ansible/compat/__init__.py
Normal file
27
lib/ansible/compat/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# (c) 2014, 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
|
||||
|
||||
'''
|
||||
Compat library for ansible. This contains compatibility definitions for older python
|
||||
When we need to import a module differently depending on python version, do it
|
||||
here. Then in the code we can simply import from compat in order to get what we want.
|
||||
'''
|
||||
|
||||
40
lib/ansible/compat/tests/__init__.py
Normal file
40
lib/ansible/compat/tests/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# (c) 2014, 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
|
||||
|
||||
'''
|
||||
This module contains things that are only needed for compat in the testsuites,
|
||||
not in ansible itself. If you are not installing the test suite, you can
|
||||
safely remove this subdirectory.
|
||||
'''
|
||||
|
||||
#
|
||||
# Compat for python2.7
|
||||
#
|
||||
|
||||
# One unittest needs to import builtins via __import__() so we need to have
|
||||
# the string that represents it
|
||||
try:
|
||||
import __builtin__
|
||||
except ImportError:
|
||||
BUILTINS = 'builtins'
|
||||
else:
|
||||
BUILTINS = '__builtin__'
|
||||
|
||||
38
lib/ansible/compat/tests/mock.py
Normal file
38
lib/ansible/compat/tests/mock.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# (c) 2014, 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
|
||||
|
||||
'''
|
||||
Compat module for Python3.x's unittest.mock module
|
||||
'''
|
||||
|
||||
# Python 2.7
|
||||
|
||||
# Note: Could use the pypi mock library on python3.x as well as python2.x. It
|
||||
# is the same as the python3 stdlib mock library
|
||||
|
||||
try:
|
||||
from unittest.mock import *
|
||||
except ImportError:
|
||||
# Python 2
|
||||
try:
|
||||
from mock import *
|
||||
except ImportError:
|
||||
print('You need the mock library installed on python2.x to run tests')
|
||||
36
lib/ansible/compat/tests/unittest.py
Normal file
36
lib/ansible/compat/tests/unittest.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# (c) 2014, 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
|
||||
|
||||
'''
|
||||
Compat module for Python2.7's unittest module
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
# Python 2.6
|
||||
if sys.version_info < (2, 7):
|
||||
try:
|
||||
# Need unittest2 on python2.6
|
||||
from unittest2 import *
|
||||
except ImportError:
|
||||
print('You need unittest2 installed on python2.6.x to run tests')
|
||||
else:
|
||||
from unittest import *
|
||||
20
lib/ansible/config/__init__.py
Normal file
20
lib/ansible/config/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# (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
|
||||
@@ -15,10 +15,15 @@
|
||||
# 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
|
||||
import pwd
|
||||
import sys
|
||||
import ConfigParser
|
||||
|
||||
from six.moves import configparser
|
||||
from string import ascii_letters, digits
|
||||
|
||||
# copied from utils, avoid circular reference fun :)
|
||||
@@ -35,13 +40,15 @@ def get_config(p, section, key, env_var, default, boolean=False, integer=False,
|
||||
''' return a configuration variable with casting '''
|
||||
value = _get_config(p, section, key, env_var, default)
|
||||
if boolean:
|
||||
return mk_boolean(value)
|
||||
if value and integer:
|
||||
return int(value)
|
||||
if value and floating:
|
||||
return float(value)
|
||||
if value and islist:
|
||||
return [x.strip() for x in value.split(',')]
|
||||
value = mk_boolean(value)
|
||||
if value:
|
||||
if integer:
|
||||
value = int(value)
|
||||
elif floating:
|
||||
value = float(value)
|
||||
elif islist:
|
||||
if isinstance(value, basestring):
|
||||
value = [x.strip() for x in value.split(',')]
|
||||
return value
|
||||
|
||||
def _get_config(p, section, key, env_var, default):
|
||||
@@ -60,7 +67,7 @@ def _get_config(p, section, key, env_var, default):
|
||||
def load_config_file():
|
||||
''' Load Config File order(first found is used): ENV, CWD, HOME, /etc/ansible '''
|
||||
|
||||
p = ConfigParser.ConfigParser()
|
||||
p = configparser.ConfigParser()
|
||||
|
||||
path0 = os.getenv("ANSIBLE_CONFIG", None)
|
||||
if path0 is not None:
|
||||
@@ -73,8 +80,8 @@ def load_config_file():
|
||||
if path is not None and os.path.exists(path):
|
||||
try:
|
||||
p.read(path)
|
||||
except ConfigParser.Error as e:
|
||||
print "Error reading config file: \n%s" % e
|
||||
except configparser.Error as e:
|
||||
print("Error reading config file: \n{0}".format(e))
|
||||
sys.exit(1)
|
||||
return p
|
||||
return None
|
||||
@@ -98,7 +105,8 @@ YAML_FILENAME_EXTENSIONS = [ "", ".yml", ".yaml", ".json" ]
|
||||
DEFAULTS='defaults'
|
||||
|
||||
# configurable things
|
||||
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'inventory', 'ANSIBLE_INVENTORY', get_config(p, DEFAULTS,'hostfile','ANSIBLE_HOSTS', '/etc/ansible/hosts')))
|
||||
DEFAULT_DEBUG = get_config(p, DEFAULTS, 'debug', 'ANSIBLE_DEBUG', False, boolean=True)
|
||||
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'hostfile', 'ANSIBLE_HOSTS', get_config(p, DEFAULTS,'inventory','ANSIBLE_INVENTORY', '/etc/ansible/hosts')))
|
||||
DEFAULT_MODULE_PATH = get_config(p, DEFAULTS, 'library', 'ANSIBLE_LIBRARY', None)
|
||||
DEFAULT_ROLES_PATH = shell_expand_path(get_config(p, DEFAULTS, 'roles_path', 'ANSIBLE_ROLES_PATH', '/etc/ansible/roles'))
|
||||
DEFAULT_REMOTE_TMP = get_config(p, DEFAULTS, 'remote_tmp', 'ANSIBLE_REMOTE_TEMP', '$HOME/.ansible/tmp')
|
||||
@@ -112,6 +120,7 @@ DEFAULT_POLL_INTERVAL = get_config(p, DEFAULTS, 'poll_interval', 'ANSIBLE
|
||||
DEFAULT_REMOTE_USER = get_config(p, DEFAULTS, 'remote_user', 'ANSIBLE_REMOTE_USER', active_user)
|
||||
DEFAULT_ASK_PASS = get_config(p, DEFAULTS, 'ask_pass', 'ANSIBLE_ASK_PASS', False, boolean=True)
|
||||
DEFAULT_PRIVATE_KEY_FILE = shell_expand_path(get_config(p, DEFAULTS, 'private_key_file', 'ANSIBLE_PRIVATE_KEY_FILE', None))
|
||||
DEFAULT_SUDO_USER = get_config(p, DEFAULTS, 'sudo_user', 'ANSIBLE_SUDO_USER', 'root')
|
||||
DEFAULT_ASK_SUDO_PASS = get_config(p, DEFAULTS, 'ask_sudo_pass', 'ANSIBLE_ASK_SUDO_PASS', False, boolean=True)
|
||||
DEFAULT_REMOTE_PORT = get_config(p, DEFAULTS, 'remote_port', 'ANSIBLE_REMOTE_PORT', None, integer=True)
|
||||
DEFAULT_ASK_VAULT_PASS = get_config(p, DEFAULTS, 'ask_vault_pass', 'ANSIBLE_ASK_VAULT_PASS', False, boolean=True)
|
||||
@@ -122,7 +131,6 @@ DEFAULT_MANAGED_STR = get_config(p, DEFAULTS, 'ansible_managed', None,
|
||||
DEFAULT_SYSLOG_FACILITY = get_config(p, DEFAULTS, 'syslog_facility', 'ANSIBLE_SYSLOG_FACILITY', 'LOG_USER')
|
||||
DEFAULT_KEEP_REMOTE_FILES = get_config(p, DEFAULTS, 'keep_remote_files', 'ANSIBLE_KEEP_REMOTE_FILES', False, boolean=True)
|
||||
DEFAULT_SUDO = get_config(p, DEFAULTS, 'sudo', 'ANSIBLE_SUDO', False, boolean=True)
|
||||
DEFAULT_SUDO_USER = get_config(p, DEFAULTS, 'sudo_user', 'ANSIBLE_SUDO_USER', 'root')
|
||||
DEFAULT_SUDO_EXE = get_config(p, DEFAULTS, 'sudo_exe', 'ANSIBLE_SUDO_EXE', 'sudo')
|
||||
DEFAULT_SUDO_FLAGS = get_config(p, DEFAULTS, 'sudo_flags', 'ANSIBLE_SUDO_FLAGS', '-H')
|
||||
DEFAULT_HASH_BEHAVIOUR = get_config(p, DEFAULTS, 'hash_behaviour', 'ANSIBLE_HASH_BEHAVIOUR', 'replace')
|
||||
@@ -144,7 +152,7 @@ BECOME_METHODS = ['sudo','su','pbrun','pfexec','runas']
|
||||
BECOME_ERROR_STRINGS = {'sudo': 'Sorry, try again.', 'su': 'Authentication failure', 'pbrun': '', 'pfexec': '', 'runas': ''}
|
||||
DEFAULT_BECOME = get_config(p, 'privilege_escalation', 'become', 'ANSIBLE_BECOME',False, boolean=True)
|
||||
DEFAULT_BECOME_METHOD = get_config(p, 'privilege_escalation', 'become_method', 'ANSIBLE_BECOME_METHOD','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo' ).lower()
|
||||
DEFAULT_BECOME_USER = get_config(p, 'privilege_escalation', 'become_user', 'ANSIBLE_BECOME_USER',default=None)
|
||||
DEFAULT_BECOME_USER = get_config(p, 'privilege_escalation', 'become_user', 'ANSIBLE_BECOME_USER', 'root')
|
||||
DEFAULT_BECOME_ASK_PASS = get_config(p, 'privilege_escalation', 'become_ask_pass', 'ANSIBLE_BECOME_ASK_PASS', False, boolean=True)
|
||||
# need to rethink impementing these 2
|
||||
DEFAULT_BECOME_EXE = None
|
||||
@@ -159,6 +167,7 @@ DEFAULT_CONNECTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'connection_plugins', '
|
||||
DEFAULT_LOOKUP_PLUGIN_PATH = get_config(p, DEFAULTS, 'lookup_plugins', 'ANSIBLE_LOOKUP_PLUGINS', '~/.ansible/plugins/lookup_plugins:/usr/share/ansible_plugins/lookup_plugins')
|
||||
DEFAULT_VARS_PLUGIN_PATH = get_config(p, DEFAULTS, 'vars_plugins', 'ANSIBLE_VARS_PLUGINS', '~/.ansible/plugins/vars_plugins:/usr/share/ansible_plugins/vars_plugins')
|
||||
DEFAULT_FILTER_PLUGIN_PATH = get_config(p, DEFAULTS, 'filter_plugins', 'ANSIBLE_FILTER_PLUGINS', '~/.ansible/plugins/filter_plugins:/usr/share/ansible_plugins/filter_plugins')
|
||||
DEFAULT_STDOUT_CALLBACK = get_config(p, DEFAULTS, 'stdout_callback', 'ANSIBLE_STDOUT_CALLBACK', 'default')
|
||||
|
||||
CACHE_PLUGIN = get_config(p, DEFAULTS, 'fact_caching', 'ANSIBLE_CACHE_PLUGIN', 'memory')
|
||||
CACHE_PLUGIN_CONNECTION = get_config(p, DEFAULTS, 'fact_caching_connection', 'ANSIBLE_CACHE_PLUGIN_CONNECTION', None)
|
||||
@@ -176,9 +185,6 @@ DEPRECATION_WARNINGS = get_config(p, DEFAULTS, 'deprecation_warnings',
|
||||
DEFAULT_CALLABLE_WHITELIST = get_config(p, DEFAULTS, 'callable_whitelist', 'ANSIBLE_CALLABLE_WHITELIST', [], islist=True)
|
||||
COMMAND_WARNINGS = get_config(p, DEFAULTS, 'command_warnings', 'ANSIBLE_COMMAND_WARNINGS', False, boolean=True)
|
||||
DEFAULT_LOAD_CALLBACK_PLUGINS = get_config(p, DEFAULTS, 'bin_ansible_callbacks', 'ANSIBLE_LOAD_CALLBACK_PLUGINS', False, boolean=True)
|
||||
DEFAULT_FORCE_HANDLERS = get_config(p, DEFAULTS, 'force_handlers', 'ANSIBLE_FORCE_HANDLERS', False, boolean=True)
|
||||
|
||||
|
||||
RETRY_FILES_ENABLED = get_config(p, DEFAULTS, 'retry_files_enabled', 'ANSIBLE_RETRY_FILES_ENABLED', True, boolean=True)
|
||||
RETRY_FILES_SAVE_PATH = get_config(p, DEFAULTS, 'retry_files_save_path', 'ANSIBLE_RETRY_FILES_SAVE_PATH', '~/')
|
||||
|
||||
@@ -186,7 +192,9 @@ RETRY_FILES_SAVE_PATH = get_config(p, DEFAULTS, 'retry_files_save_path'
|
||||
ANSIBLE_SSH_ARGS = get_config(p, 'ssh_connection', 'ssh_args', 'ANSIBLE_SSH_ARGS', None)
|
||||
ANSIBLE_SSH_CONTROL_PATH = get_config(p, 'ssh_connection', 'control_path', 'ANSIBLE_SSH_CONTROL_PATH', "%(directory)s/ansible-ssh-%%h-%%p-%%r")
|
||||
ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, boolean=True)
|
||||
ANSIBLE_SSH_RETRIES = get_config(p, 'ssh_connection', 'retries', 'ANSIBLE_SSH_RETRIES', 0, integer=True)
|
||||
PARAMIKO_RECORD_HOST_KEYS = get_config(p, 'paramiko_connection', 'record_host_keys', 'ANSIBLE_PARAMIKO_RECORD_HOST_KEYS', True, boolean=True)
|
||||
|
||||
# obsolete -- will be formally removed
|
||||
ZEROMQ_PORT = get_config(p, 'fireball_connection', 'zeromq_port', 'ANSIBLE_ZEROMQ_PORT', 5099, integer=True)
|
||||
ACCELERATE_PORT = get_config(p, 'accelerate', 'accelerate_port', 'ACCELERATE_PORT', 5099, integer=True)
|
||||
@@ -199,10 +207,16 @@ ACCELERATE_KEYS_FILE_PERMS = get_config(p, 'accelerate', 'accelerate_keys_fi
|
||||
ACCELERATE_MULTI_KEY = get_config(p, 'accelerate', 'accelerate_multi_key', 'ACCELERATE_MULTI_KEY', False, boolean=True)
|
||||
PARAMIKO_PTY = get_config(p, 'paramiko_connection', 'pty', 'ANSIBLE_PARAMIKO_PTY', True, boolean=True)
|
||||
|
||||
# galaxy related
|
||||
DEFAULT_GALAXY_URI = get_config(p, 'galaxy', 'server_uri', 'ANSIBLE_GALAXY_SERVER_URI', 'https://galaxy.ansible.com')
|
||||
# this can be configured to blacklist SCMS but cannot add new ones unless the code is also updated
|
||||
GALAXY_SCMS = get_config(p, 'galaxy', 'scms', 'ANSIBLE_GALAXY_SCMS', ['git','hg'], islist=True)
|
||||
|
||||
# characters included in auto-generated passwords
|
||||
DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
|
||||
|
||||
# non-configurable things
|
||||
MODULE_REQUIRE_ARGS = ['command', 'shell', 'raw', 'script']
|
||||
DEFAULT_BECOME_PASS = None
|
||||
DEFAULT_SUDO_PASS = None
|
||||
DEFAULT_REMOTE_PASS = None
|
||||
|
||||
185
lib/ansible/errors/__init__.py
Normal file
185
lib/ansible/errors/__init__.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# (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 ansible.errors.yaml_strings import *
|
||||
|
||||
class AnsibleError(Exception):
|
||||
'''
|
||||
This is the base class for all errors raised from Ansible code,
|
||||
and can be instantiated with two optional parameters beyond the
|
||||
error message to control whether detailed information is displayed
|
||||
when the error occurred while parsing a data file of some kind.
|
||||
|
||||
Usage:
|
||||
|
||||
raise AnsibleError('some message here', obj=obj, show_content=True)
|
||||
|
||||
Where "obj" is some subclass of ansible.parsing.yaml.objects.AnsibleBaseYAMLObject,
|
||||
which should be returned by the DataLoader() class.
|
||||
'''
|
||||
|
||||
def __init__(self, message, obj=None, show_content=True):
|
||||
# we import this here to prevent an import loop problem,
|
||||
# since the objects code also imports ansible.errors
|
||||
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
|
||||
|
||||
self._obj = obj
|
||||
self._show_content = show_content
|
||||
if obj and isinstance(obj, AnsibleBaseYAMLObject):
|
||||
extended_error = self._get_extended_error()
|
||||
if extended_error:
|
||||
self.message = 'ERROR! %s\n\n%s' % (message, extended_error)
|
||||
else:
|
||||
self.message = 'ERROR! %s' % message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def __repr__(self):
|
||||
return self.message
|
||||
|
||||
def _get_error_lines_from_file(self, file_name, line_number):
|
||||
'''
|
||||
Returns the line in the file which coresponds to the reported error
|
||||
location, as well as the line preceding it (if the error did not
|
||||
occur on the first line), to provide context to the error.
|
||||
'''
|
||||
|
||||
target_line = ''
|
||||
prev_line = ''
|
||||
|
||||
with open(file_name, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
target_line = lines[line_number]
|
||||
if line_number > 0:
|
||||
prev_line = lines[line_number - 1]
|
||||
|
||||
return (target_line, prev_line)
|
||||
|
||||
def _get_extended_error(self):
|
||||
'''
|
||||
Given an object reporting the location of the exception in a file, return
|
||||
detailed information regarding it including:
|
||||
|
||||
* the line which caused the error as well as the one preceding it
|
||||
* causes and suggested remedies for common syntax errors
|
||||
|
||||
If this error was created with show_content=False, the reporting of content
|
||||
is suppressed, as the file contents may be sensitive (ie. vault data).
|
||||
'''
|
||||
|
||||
error_message = ''
|
||||
|
||||
try:
|
||||
(src_file, line_number, col_number) = self._obj.ansible_pos
|
||||
error_message += YAML_POSITION_DETAILS % (src_file, line_number, col_number)
|
||||
if src_file not in ('<string>', '<unicode>') and self._show_content:
|
||||
(target_line, prev_line) = self._get_error_lines_from_file(src_file, line_number - 1)
|
||||
if target_line:
|
||||
stripped_line = target_line.replace(" ","")
|
||||
arrow_line = (" " * (col_number-1)) + "^ here"
|
||||
#header_line = ("=" * 73)
|
||||
error_message += "\nThe offending line appears to be:\n\n%s\n%s\n%s\n" % (prev_line.rstrip(), target_line.rstrip(), arrow_line)
|
||||
|
||||
# common error/remediation checking here:
|
||||
# check for unquoted vars starting lines
|
||||
if ('{{' in target_line and '}}' in target_line) and ('"{{' not in target_line or "'{{" not in target_line):
|
||||
error_message += YAML_COMMON_UNQUOTED_VARIABLE_ERROR
|
||||
# check for common dictionary mistakes
|
||||
elif ":{{" in stripped_line and "}}" in stripped_line:
|
||||
error_message += YAML_COMMON_DICT_ERROR
|
||||
# check for common unquoted colon mistakes
|
||||
elif len(target_line) and len(target_line) > 1 and len(target_line) > col_number and target_line[col_number] == ":" and target_line.count(':') > 1:
|
||||
error_message += YAML_COMMON_UNQUOTED_COLON_ERROR
|
||||
# otherwise, check for some common quoting mistakes
|
||||
else:
|
||||
parts = target_line.split(":")
|
||||
if len(parts) > 1:
|
||||
middle = parts[1].strip()
|
||||
match = False
|
||||
unbalanced = False
|
||||
|
||||
if middle.startswith("'") and not middle.endswith("'"):
|
||||
match = True
|
||||
elif middle.startswith('"') and not middle.endswith('"'):
|
||||
match = True
|
||||
|
||||
if len(middle) > 0 and middle[0] in [ '"', "'" ] and middle[-1] in [ '"', "'" ] and target_line.count("'") > 2 or target_line.count('"') > 2:
|
||||
unbalanced = True
|
||||
|
||||
if match:
|
||||
error_message += YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR
|
||||
if unbalanced:
|
||||
error_message += YAML_COMMON_UNBALANCED_QUOTES_ERROR
|
||||
|
||||
except (IOError, TypeError):
|
||||
error_message += '\n(could not open file to display line)'
|
||||
except IndexError:
|
||||
error_message += '\n(specified line no longer in file, maybe it changed?)'
|
||||
|
||||
return error_message
|
||||
|
||||
class AnsibleOptionsError(AnsibleError):
|
||||
''' bad or incomplete options passed '''
|
||||
pass
|
||||
|
||||
class AnsibleParserError(AnsibleError):
|
||||
''' something was detected early that is wrong about a playbook or data file '''
|
||||
pass
|
||||
|
||||
class AnsibleInternalError(AnsibleError):
|
||||
''' internal safeguards tripped, something happened in the code that should never happen '''
|
||||
pass
|
||||
|
||||
class AnsibleRuntimeError(AnsibleError):
|
||||
''' ansible had a problem while running a playbook '''
|
||||
pass
|
||||
|
||||
class AnsibleModuleError(AnsibleRuntimeError):
|
||||
''' a module failed somehow '''
|
||||
pass
|
||||
|
||||
class AnsibleConnectionFailure(AnsibleRuntimeError):
|
||||
''' the transport / connection_plugin had a fatal error '''
|
||||
pass
|
||||
|
||||
class AnsibleFilterError(AnsibleRuntimeError):
|
||||
''' a templating failure '''
|
||||
pass
|
||||
|
||||
class AnsibleLookupError(AnsibleRuntimeError):
|
||||
''' a lookup failure '''
|
||||
pass
|
||||
|
||||
class AnsibleCallbackError(AnsibleRuntimeError):
|
||||
''' a callback failure '''
|
||||
pass
|
||||
|
||||
class AnsibleUndefinedVariable(AnsibleRuntimeError):
|
||||
''' a templating failure '''
|
||||
pass
|
||||
|
||||
class AnsibleFileNotFound(AnsibleRuntimeError):
|
||||
''' a file missing failure '''
|
||||
pass
|
||||
118
lib/ansible/errors/yaml_strings.py
Normal file
118
lib/ansible/errors/yaml_strings.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# (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__ = [
|
||||
'YAML_SYNTAX_ERROR',
|
||||
'YAML_POSITION_DETAILS',
|
||||
'YAML_COMMON_DICT_ERROR',
|
||||
'YAML_COMMON_UNQUOTED_VARIABLE_ERROR',
|
||||
'YAML_COMMON_UNQUOTED_COLON_ERROR',
|
||||
'YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR',
|
||||
'YAML_COMMON_UNBALANCED_QUOTES_ERROR',
|
||||
]
|
||||
|
||||
YAML_SYNTAX_ERROR = """\
|
||||
Syntax Error while loading YAML.
|
||||
"""
|
||||
|
||||
YAML_POSITION_DETAILS = """\
|
||||
The error appears to have been in '%s': line %s, column %s, but may
|
||||
be elsewhere in the file depending on the exact syntax problem.
|
||||
"""
|
||||
|
||||
YAML_COMMON_DICT_ERROR = """\
|
||||
This one looks easy to fix. YAML thought it was looking for the start of a
|
||||
hash/dictionary and was confused to see a second "{". Most likely this was
|
||||
meant to be an ansible template evaluation instead, so we have to give the
|
||||
parser a small hint that we wanted a string instead. The solution here is to
|
||||
just quote the entire value.
|
||||
|
||||
For instance, if the original line was:
|
||||
|
||||
app_path: {{ base_path }}/foo
|
||||
|
||||
It should be written as:
|
||||
|
||||
app_path: "{{ base_path }}/foo"
|
||||
"""
|
||||
|
||||
YAML_COMMON_UNQUOTED_VARIABLE_ERROR = """\
|
||||
We could be wrong, but this one looks like it might be an issue with
|
||||
missing quotes. Always quote template expression brackets when they
|
||||
start a value. For instance:
|
||||
|
||||
with_items:
|
||||
- {{ foo }}
|
||||
|
||||
Should be written as:
|
||||
|
||||
with_items:
|
||||
- "{{ foo }}"
|
||||
"""
|
||||
|
||||
YAML_COMMON_UNQUOTED_COLON_ERROR = """\
|
||||
This one looks easy to fix. There seems to be an extra unquoted colon in the line
|
||||
and this is confusing the parser. It was only expecting to find one free
|
||||
colon. The solution is just add some quotes around the colon, or quote the
|
||||
entire line after the first colon.
|
||||
|
||||
For instance, if the original line was:
|
||||
|
||||
copy: src=file.txt dest=/path/filename:with_colon.txt
|
||||
|
||||
It can be written as:
|
||||
|
||||
copy: src=file.txt dest='/path/filename:with_colon.txt'
|
||||
|
||||
Or:
|
||||
|
||||
copy: 'src=file.txt dest=/path/filename:with_colon.txt'
|
||||
"""
|
||||
|
||||
YAML_COMMON_PARTIALLY_QUOTED_LINE_ERROR = """\
|
||||
This one looks easy to fix. It seems that there is a value started
|
||||
with a quote, and the YAML parser is expecting to see the line ended
|
||||
with the same kind of quote. For instance:
|
||||
|
||||
when: "ok" in result.stdout
|
||||
|
||||
Could be written as:
|
||||
|
||||
when: '"ok" in result.stdout'
|
||||
|
||||
Or equivalently:
|
||||
|
||||
when: "'ok' in result.stdout"
|
||||
"""
|
||||
|
||||
YAML_COMMON_UNBALANCED_QUOTES_ERROR = """\
|
||||
We could be wrong, but this one looks like it might be an issue with
|
||||
unbalanced quotes. If starting a value with a quote, make sure the
|
||||
line ends with the same set of quotes. For instance this arbitrary
|
||||
example:
|
||||
|
||||
foo: "bad" "wolf"
|
||||
|
||||
Could be written as:
|
||||
|
||||
foo: '"bad" "wolf"'
|
||||
"""
|
||||
|
||||
21
lib/ansible/executor/__init__.py
Normal file
21
lib/ansible/executor/__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
|
||||
|
||||
287
lib/ansible/executor/connection_info.py
Normal file
287
lib/ansible/executor/connection_info.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# (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 pipes
|
||||
import random
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.template import Templar
|
||||
from ansible.utils.boolean import boolean
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
__all__ = ['ConnectionInformation']
|
||||
|
||||
# the magic variable mapping dictionary below is used to translate
|
||||
# host/inventory variables to fields in the ConnectionInformation
|
||||
# object. The dictionary values are tuples, to account for aliases
|
||||
# in variable names.
|
||||
|
||||
MAGIC_VARIABLE_MAPPING = dict(
|
||||
connection = ('ansible_connection',),
|
||||
remote_addr = ('ansible_ssh_host', 'ansible_host'),
|
||||
remote_user = ('ansible_ssh_user', 'ansible_user'),
|
||||
port = ('ansible_ssh_port', 'ansible_port'),
|
||||
password = ('ansible_ssh_pass', 'ansible_password'),
|
||||
private_key_file = ('ansible_ssh_private_key_file', 'ansible_private_key_file'),
|
||||
shell = ('ansible_shell_type',),
|
||||
)
|
||||
|
||||
class ConnectionInformation:
|
||||
|
||||
'''
|
||||
This class is used to consolidate the connection information for
|
||||
hosts in a play and child tasks, where the task may override some
|
||||
connection/authentication information.
|
||||
'''
|
||||
|
||||
def __init__(self, play=None, options=None, passwords=None):
|
||||
|
||||
if passwords is None:
|
||||
passwords = {}
|
||||
|
||||
# connection
|
||||
self.connection = None
|
||||
self.remote_addr = None
|
||||
self.remote_user = None
|
||||
self.password = passwords.get('conn_pass','')
|
||||
self.port = None
|
||||
self.private_key_file = C.DEFAULT_PRIVATE_KEY_FILE
|
||||
self.timeout = C.DEFAULT_TIMEOUT
|
||||
self.shell = None
|
||||
|
||||
# privilege escalation
|
||||
self.become = None
|
||||
self.become_method = None
|
||||
self.become_user = None
|
||||
self.become_pass = passwords.get('become_pass','')
|
||||
|
||||
# general flags (should we move out?)
|
||||
self.verbosity = 0
|
||||
self.only_tags = set()
|
||||
self.skip_tags = set()
|
||||
self.no_log = False
|
||||
self.check_mode = False
|
||||
|
||||
#TODO: just pull options setup to above?
|
||||
# set options before play to allow play to override them
|
||||
if options:
|
||||
self.set_options(options)
|
||||
|
||||
if play:
|
||||
self.set_play(play)
|
||||
|
||||
def set_play(self, play):
|
||||
'''
|
||||
Configures this connection information instance with data from
|
||||
the play class.
|
||||
'''
|
||||
|
||||
if play.connection:
|
||||
self.connection = play.connection
|
||||
|
||||
if play.remote_user:
|
||||
self.remote_user = play.remote_user
|
||||
|
||||
if play.port:
|
||||
self.port = int(play.port)
|
||||
|
||||
if play.become is not None:
|
||||
self.become = play.become
|
||||
if play.become_method:
|
||||
self.become_method = play.become_method
|
||||
if play.become_user:
|
||||
self.become_user = play.become_user
|
||||
self.become_pass = play.become_pass
|
||||
|
||||
# non connection related
|
||||
self.no_log = play.no_log
|
||||
self.environment = play.environment
|
||||
|
||||
def set_options(self, options):
|
||||
'''
|
||||
Configures this connection information instance with data from
|
||||
options specified by the user on the command line. These have a
|
||||
higher precedence than those set on the play or host.
|
||||
'''
|
||||
|
||||
if options.connection:
|
||||
self.connection = options.connection
|
||||
|
||||
self.remote_user = options.remote_user
|
||||
self.private_key_file = options.private_key_file
|
||||
|
||||
# privilege escalation
|
||||
self.become = options.become
|
||||
self.become_method = options.become_method
|
||||
self.become_user = options.become_user
|
||||
self.become_pass = ''
|
||||
|
||||
# general flags (should we move out?)
|
||||
if options.verbosity:
|
||||
self.verbosity = options.verbosity
|
||||
#if options.no_log:
|
||||
# self.no_log = boolean(options.no_log)
|
||||
if options.check:
|
||||
self.check_mode = boolean(options.check)
|
||||
|
||||
# get the tag info from options, converting a comma-separated list
|
||||
# of values into a proper list if need be. We check to see if the
|
||||
# options have the attribute, as it is not always added via the CLI
|
||||
if hasattr(options, 'tags'):
|
||||
if isinstance(options.tags, list):
|
||||
self.only_tags.update(options.tags)
|
||||
elif isinstance(options.tags, basestring):
|
||||
self.only_tags.update(options.tags.split(','))
|
||||
|
||||
if len(self.only_tags) == 0:
|
||||
self.only_tags = set(['all'])
|
||||
|
||||
if hasattr(options, 'skip_tags'):
|
||||
if isinstance(options.skip_tags, list):
|
||||
self.skip_tags.update(options.skip_tags)
|
||||
elif isinstance(options.skip_tags, basestring):
|
||||
self.skip_tags.update(options.skip_tags.split(','))
|
||||
|
||||
def copy(self, ci):
|
||||
'''
|
||||
Copies the connection info from another connection info object, used
|
||||
when merging in data from task overrides.
|
||||
'''
|
||||
|
||||
for field in self._get_fields():
|
||||
value = getattr(ci, field, None)
|
||||
if isinstance(value, dict):
|
||||
setattr(self, field, value.copy())
|
||||
elif isinstance(value, set):
|
||||
setattr(self, field, value.copy())
|
||||
elif isinstance(value, list):
|
||||
setattr(self, field, value[:])
|
||||
else:
|
||||
setattr(self, field, value)
|
||||
|
||||
def set_task_and_host_override(self, task, host):
|
||||
'''
|
||||
Sets attributes from the task if they are set, which will override
|
||||
those from the play.
|
||||
'''
|
||||
|
||||
new_info = ConnectionInformation()
|
||||
new_info.copy(self)
|
||||
|
||||
# loop through a subset of attributes on the task object and set
|
||||
# connection fields based on their values
|
||||
for attr in ('connection', 'remote_user', 'become', 'become_user', 'become_pass', 'become_method', 'environment', 'no_log'):
|
||||
if hasattr(task, attr):
|
||||
attr_val = getattr(task, attr)
|
||||
if attr_val is not None:
|
||||
setattr(new_info, attr, attr_val)
|
||||
|
||||
# finally, use the MAGIC_VARIABLE_MAPPING dictionary to update this
|
||||
# connection info object with 'magic' variables from inventory
|
||||
variables = host.get_vars()
|
||||
for (attr, variable_names) in MAGIC_VARIABLE_MAPPING.iteritems():
|
||||
for variable_name in variable_names:
|
||||
if variable_name in variables:
|
||||
setattr(new_info, attr, variables[variable_name])
|
||||
|
||||
return new_info
|
||||
|
||||
def make_become_cmd(self, cmd, executable, become_settings=None):
|
||||
|
||||
"""
|
||||
helper function to create privilege escalation commands
|
||||
"""
|
||||
|
||||
# FIXME: become settings should probably be stored in the connection info itself
|
||||
if become_settings is None:
|
||||
become_settings = {}
|
||||
|
||||
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||
success_key = 'BECOME-SUCCESS-%s' % randbits
|
||||
prompt = None
|
||||
becomecmd = None
|
||||
|
||||
executable = executable or '$SHELL'
|
||||
|
||||
success_cmd = pipes.quote('echo %s; %s' % (success_key, cmd))
|
||||
if self.become:
|
||||
if self.become_method == 'sudo':
|
||||
# Rather than detect if sudo wants a password this time, -k makes sudo always ask for
|
||||
# a password if one is required. Passing a quoted compound command to sudo (or sudo -s)
|
||||
# directly doesn't work, so we shellquote it with pipes.quote() and pass the quoted
|
||||
# string to the user's shell. We loop reading output until we see the randomly-generated
|
||||
# sudo prompt set with the -p option.
|
||||
prompt = '[sudo via ansible, key=%s] password: ' % randbits
|
||||
exe = become_settings.get('sudo_exe', C.DEFAULT_SUDO_EXE)
|
||||
flags = become_settings.get('sudo_flags', C.DEFAULT_SUDO_FLAGS)
|
||||
becomecmd = '%s -k && %s %s -S -p "%s" -u %s %s -c %s' % \
|
||||
(exe, exe, flags or C.DEFAULT_SUDO_FLAGS, prompt, self.become_user, executable, success_cmd)
|
||||
|
||||
elif self.become_method == 'su':
|
||||
exe = become_settings.get('su_exe', C.DEFAULT_SU_EXE)
|
||||
flags = become_settings.get('su_flags', C.DEFAULT_SU_FLAGS)
|
||||
becomecmd = '%s %s %s -c "%s -c %s"' % (exe, flags, self.become_user, executable, success_cmd)
|
||||
|
||||
elif self.become_method == 'pbrun':
|
||||
exe = become_settings.get('pbrun_exe', 'pbrun')
|
||||
flags = become_settings.get('pbrun_flags', '')
|
||||
becomecmd = '%s -b -l %s -u %s %s' % (exe, flags, self.become_user, success_cmd)
|
||||
|
||||
elif self.become_method == 'pfexec':
|
||||
exe = become_settings.get('pfexec_exe', 'pbrun')
|
||||
flags = become_settings.get('pfexec_flags', '')
|
||||
# No user as it uses it's own exec_attr to figure it out
|
||||
becomecmd = '%s %s "%s"' % (exe, flags, success_cmd)
|
||||
|
||||
else:
|
||||
raise AnsibleError("Privilege escalation method not found: %s" % self.become_method)
|
||||
|
||||
return (('%s -c ' % executable) + pipes.quote(becomecmd), prompt, success_key)
|
||||
|
||||
return (cmd, "", "")
|
||||
|
||||
def check_become_success(self, output, become_settings):
|
||||
#TODO: implement
|
||||
pass
|
||||
|
||||
def _get_fields(self):
|
||||
return [i for i in self.__dict__.keys() if i[:1] != '_']
|
||||
|
||||
def post_validate(self, templar):
|
||||
'''
|
||||
Finalizes templated values which may be set on this objects fields.
|
||||
'''
|
||||
|
||||
for field in self._get_fields():
|
||||
value = templar.template(getattr(self, field))
|
||||
setattr(self, field, value)
|
||||
|
||||
def update_vars(self, variables):
|
||||
'''
|
||||
Adds 'magic' variables relating to connections to the variable dictionary provided.
|
||||
'''
|
||||
|
||||
variables['ansible_connection'] = self.connection
|
||||
variables['ansible_ssh_host'] = self.remote_addr
|
||||
variables['ansible_ssh_pass'] = self.password
|
||||
variables['ansible_ssh_port'] = self.port
|
||||
variables['ansible_ssh_user'] = self.remote_user
|
||||
variables['ansible_ssh_private_key_file'] = self.private_key_file
|
||||
199
lib/ansible/executor/module_common.py
Normal file
199
lib/ansible/executor/module_common.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# (c) 2013-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 python and deps
|
||||
from six.moves import StringIO
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
|
||||
# from Ansible
|
||||
from ansible import __version__
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.parsing.utils.jsonify import jsonify
|
||||
|
||||
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
|
||||
REPLACER_ARGS = "\"<<INCLUDE_ANSIBLE_MODULE_ARGS>>\""
|
||||
REPLACER_COMPLEX = "\"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>\""
|
||||
REPLACER_WINDOWS = "# POWERSHELL_COMMON"
|
||||
REPLACER_VERSION = "\"<<ANSIBLE_VERSION>>\""
|
||||
|
||||
# We could end up writing out parameters with unicode characters so we need to
|
||||
# specify an encoding for the python source file
|
||||
ENCODING_STRING = '# -*- coding: utf-8 -*-'
|
||||
|
||||
# we've moved the module_common relative to the snippets, so fix the path
|
||||
_SNIPPET_PATH = os.path.join(os.path.dirname(__file__), '..', 'module_utils')
|
||||
|
||||
# ******************************************************************************
|
||||
|
||||
def _slurp(path):
|
||||
if not os.path.exists(path):
|
||||
raise AnsibleError("imported module support code does not exist at %s" % path)
|
||||
fd = open(path)
|
||||
data = fd.read()
|
||||
fd.close()
|
||||
return data
|
||||
|
||||
def _find_snippet_imports(module_data, module_path, strip_comments):
|
||||
"""
|
||||
Given the source of the module, convert it to a Jinja2 template to insert
|
||||
module code and return whether it's a new or old style module.
|
||||
"""
|
||||
|
||||
module_style = 'old'
|
||||
if REPLACER in module_data:
|
||||
module_style = 'new'
|
||||
elif 'from ansible.module_utils.' in module_data:
|
||||
module_style = 'new'
|
||||
elif 'WANT_JSON' in module_data:
|
||||
module_style = 'non_native_want_json'
|
||||
|
||||
output = StringIO()
|
||||
lines = module_data.split('\n')
|
||||
snippet_names = []
|
||||
|
||||
for line in lines:
|
||||
|
||||
if REPLACER in line:
|
||||
output.write(_slurp(os.path.join(_SNIPPET_PATH, "basic.py")))
|
||||
snippet_names.append('basic')
|
||||
if REPLACER_WINDOWS in line:
|
||||
ps_data = _slurp(os.path.join(_SNIPPET_PATH, "powershell.ps1"))
|
||||
output.write(ps_data)
|
||||
snippet_names.append('powershell')
|
||||
elif line.startswith('from ansible.module_utils.'):
|
||||
tokens=line.split(".")
|
||||
import_error = False
|
||||
if len(tokens) != 3:
|
||||
import_error = True
|
||||
if " import *" not in line:
|
||||
import_error = True
|
||||
if import_error:
|
||||
raise AnsibleError("error importing module in %s, expecting format like 'from ansible.module_utils.basic import *'" % module_path)
|
||||
snippet_name = tokens[2].split()[0]
|
||||
snippet_names.append(snippet_name)
|
||||
output.write(_slurp(os.path.join(_SNIPPET_PATH, snippet_name + ".py")))
|
||||
else:
|
||||
if strip_comments and line.startswith("#") or line == '':
|
||||
pass
|
||||
output.write(line)
|
||||
output.write("\n")
|
||||
|
||||
if not module_path.endswith(".ps1"):
|
||||
# Unixy modules
|
||||
if len(snippet_names) > 0 and not 'basic' in snippet_names:
|
||||
raise AnsibleError("missing required import in %s: from ansible.module_utils.basic import *" % module_path)
|
||||
else:
|
||||
# Windows modules
|
||||
if len(snippet_names) > 0 and not 'powershell' in snippet_names:
|
||||
raise AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path)
|
||||
|
||||
return (output.getvalue(), module_style)
|
||||
|
||||
# ******************************************************************************
|
||||
|
||||
def modify_module(module_path, module_args, strip_comments=False):
|
||||
"""
|
||||
Used to insert chunks of code into modules before transfer rather than
|
||||
doing regular python imports. This allows for more efficient transfer in
|
||||
a non-bootstrapping scenario by not moving extra files over the wire and
|
||||
also takes care of embedding arguments in the transferred modules.
|
||||
|
||||
This version is done in such a way that local imports can still be
|
||||
used in the module code, so IDEs don't have to be aware of what is going on.
|
||||
|
||||
Example:
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
... will result in the insertion of basic.py into the module
|
||||
from the module_utils/ directory in the source tree.
|
||||
|
||||
All modules are required to import at least basic, though there will also
|
||||
be other snippets.
|
||||
|
||||
For powershell, there's equivalent conventions like this:
|
||||
|
||||
# POWERSHELL_COMMON
|
||||
|
||||
which results in the inclusion of the common code from powershell.ps1
|
||||
|
||||
"""
|
||||
### TODO: Optimization ideas if this code is actually a source of slowness:
|
||||
# * Fix comment stripping: Currently doesn't preserve shebangs and encoding info (but we unconditionally add encoding info)
|
||||
# * Use pyminifier if installed
|
||||
# * comment stripping/pyminifier needs to have config setting to turn it
|
||||
# off for debugging purposes (goes along with keep remote but should be
|
||||
# separate otherwise users wouldn't be able to get info on what the
|
||||
# minifier output)
|
||||
# * Only split into lines and recombine into strings once
|
||||
# * Cache the modified module? If only the args are different and we do
|
||||
# that as the last step we could cache sll the work up to that point.
|
||||
|
||||
with open(module_path) as f:
|
||||
|
||||
# read in the module source
|
||||
module_data = f.read()
|
||||
|
||||
(module_data, module_style) = _find_snippet_imports(module_data, module_path, strip_comments)
|
||||
|
||||
#module_args_json = jsonify(module_args)
|
||||
module_args_json = json.dumps(module_args)
|
||||
encoded_args = repr(module_args_json.encode('utf-8'))
|
||||
|
||||
# these strings should be part of the 'basic' snippet which is required to be included
|
||||
module_data = module_data.replace(REPLACER_VERSION, repr(__version__))
|
||||
module_data = module_data.replace(REPLACER_COMPLEX, encoded_args)
|
||||
|
||||
# FIXME: we're not passing around an inject dictionary anymore, so
|
||||
# this needs to be fixed with whatever method we use for vars
|
||||
# like this moving forward
|
||||
#if module_style == 'new':
|
||||
# facility = C.DEFAULT_SYSLOG_FACILITY
|
||||
# if 'ansible_syslog_facility' in inject:
|
||||
# facility = inject['ansible_syslog_facility']
|
||||
# module_data = module_data.replace('syslog.LOG_USER', "syslog.%s" % facility)
|
||||
|
||||
lines = module_data.split(b"\n", 1)
|
||||
shebang = None
|
||||
if lines[0].startswith(b"#!"):
|
||||
shebang = lines[0].strip()
|
||||
args = shlex.split(str(shebang[2:]))
|
||||
interpreter = args[0]
|
||||
interpreter_config = 'ansible_%s_interpreter' % os.path.basename(interpreter)
|
||||
|
||||
# FIXME: more inject stuff here...
|
||||
#from ansible.utils.unicode import to_bytes
|
||||
#if interpreter_config in inject:
|
||||
# interpreter = to_bytes(inject[interpreter_config], errors='strict')
|
||||
# lines[0] = shebang = b"#!{0} {1}".format(interpreter, b" ".join(args[1:]))
|
||||
|
||||
lines.insert(1, ENCODING_STRING)
|
||||
else:
|
||||
lines.insert(0, ENCODING_STRING)
|
||||
|
||||
module_data = b"\n".join(lines)
|
||||
|
||||
return (module_data, module_style, shebang)
|
||||
|
||||
302
lib/ansible/executor/play_iterator.py
Normal file
302
lib/ansible/executor/play_iterator.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# (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 *
|
||||
from ansible.playbook.block import Block
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
from ansible.utils.boolean import boolean
|
||||
|
||||
__all__ = ['PlayIterator']
|
||||
|
||||
class HostState:
|
||||
def __init__(self, blocks):
|
||||
self._blocks = blocks[:]
|
||||
|
||||
self.cur_block = 0
|
||||
self.cur_regular_task = 0
|
||||
self.cur_rescue_task = 0
|
||||
self.cur_always_task = 0
|
||||
self.cur_role = None
|
||||
self.run_state = PlayIterator.ITERATING_SETUP
|
||||
self.fail_state = PlayIterator.FAILED_NONE
|
||||
self.pending_setup = False
|
||||
self.child_state = None
|
||||
|
||||
def __repr__(self):
|
||||
return "HOST STATE: block=%d, task=%d, rescue=%d, always=%d, role=%s, run_state=%d, fail_state=%d, pending_setup=%s, child state? %s" % (
|
||||
self.cur_block,
|
||||
self.cur_regular_task,
|
||||
self.cur_rescue_task,
|
||||
self.cur_always_task,
|
||||
self.cur_role,
|
||||
self.run_state,
|
||||
self.fail_state,
|
||||
self.pending_setup,
|
||||
self.child_state,
|
||||
)
|
||||
|
||||
def get_current_block(self):
|
||||
return self._blocks[self.cur_block]
|
||||
|
||||
def copy(self):
|
||||
new_state = HostState(self._blocks)
|
||||
new_state.cur_block = self.cur_block
|
||||
new_state.cur_regular_task = self.cur_regular_task
|
||||
new_state.cur_rescue_task = self.cur_rescue_task
|
||||
new_state.cur_always_task = self.cur_always_task
|
||||
new_state.cur_role = self.cur_role
|
||||
new_state.run_state = self.run_state
|
||||
new_state.fail_state = self.fail_state
|
||||
new_state.pending_setup = self.pending_setup
|
||||
new_state.child_state = self.child_state
|
||||
return new_state
|
||||
|
||||
class PlayIterator:
|
||||
|
||||
# the primary running states for the play iteration
|
||||
ITERATING_SETUP = 0
|
||||
ITERATING_TASKS = 1
|
||||
ITERATING_RESCUE = 2
|
||||
ITERATING_ALWAYS = 3
|
||||
ITERATING_COMPLETE = 4
|
||||
|
||||
# the failure states for the play iteration, which are powers
|
||||
# of 2 as they may be or'ed together in certain circumstances
|
||||
FAILED_NONE = 0
|
||||
FAILED_SETUP = 1
|
||||
FAILED_TASKS = 2
|
||||
FAILED_RESCUE = 4
|
||||
FAILED_ALWAYS = 8
|
||||
|
||||
def __init__(self, inventory, play, connection_info, all_vars):
|
||||
self._play = play
|
||||
|
||||
self._blocks = []
|
||||
for block in self._play.compile():
|
||||
new_block = block.filter_tagged_tasks(connection_info, all_vars)
|
||||
if new_block.has_tasks():
|
||||
self._blocks.append(new_block)
|
||||
|
||||
self._host_states = {}
|
||||
for host in inventory.get_hosts(self._play.hosts):
|
||||
self._host_states[host.name] = HostState(blocks=self._blocks)
|
||||
|
||||
def get_host_state(self, host):
|
||||
try:
|
||||
return self._host_states[host.name].copy()
|
||||
except KeyError:
|
||||
raise AnsibleError("invalid host (%s) specified for playbook iteration" % host)
|
||||
|
||||
def get_next_task_for_host(self, host, peek=False):
|
||||
|
||||
s = self.get_host_state(host)
|
||||
|
||||
task = None
|
||||
if s.run_state == self.ITERATING_COMPLETE:
|
||||
return None
|
||||
elif s.run_state == self.ITERATING_SETUP:
|
||||
s.run_state = self.ITERATING_TASKS
|
||||
s.pending_setup = True
|
||||
if self._play.gather_facts == 'smart' and not host._gathered_facts or boolean(self._play.gather_facts):
|
||||
if not peek:
|
||||
# mark the host as having gathered facts
|
||||
host.set_gathered_facts(True)
|
||||
|
||||
task = Task()
|
||||
task.action = 'setup'
|
||||
task.args = {}
|
||||
task.set_loader(self._play._loader)
|
||||
else:
|
||||
s.pending_setup = False
|
||||
|
||||
if not task:
|
||||
(s, task) = self._get_next_task_from_state(s, peek=peek)
|
||||
|
||||
if task and task._role:
|
||||
# if we had a current role, mark that role as completed
|
||||
if s.cur_role and task._role != s.cur_role and s.cur_role._had_task_run and not peek:
|
||||
s.cur_role._completed = True
|
||||
s.cur_role = task._role
|
||||
|
||||
if not peek:
|
||||
self._host_states[host.name] = s
|
||||
|
||||
return (s, task)
|
||||
|
||||
|
||||
def _get_next_task_from_state(self, state, peek):
|
||||
|
||||
task = None
|
||||
|
||||
# if we previously encountered a child block and we have a
|
||||
# saved child state, try and get the next task from there
|
||||
if state.child_state:
|
||||
(state.child_state, task) = self._get_next_task_from_state(state.child_state, peek=peek)
|
||||
if task:
|
||||
return (state.child_state, task)
|
||||
else:
|
||||
state.child_state = None
|
||||
|
||||
# try and find the next task, given the current state.
|
||||
while True:
|
||||
# try to get the current block from the list of blocks, and
|
||||
# if we run past the end of the list we know we're done with
|
||||
# this block
|
||||
try:
|
||||
block = state._blocks[state.cur_block]
|
||||
except IndexError:
|
||||
state.run_state = self.ITERATING_COMPLETE
|
||||
return (state, None)
|
||||
|
||||
if state.run_state == self.ITERATING_TASKS:
|
||||
# clear the pending setup flag, since we're past that and it didn't fail
|
||||
if state.pending_setup:
|
||||
state.pending_setup = False
|
||||
|
||||
if state.fail_state & self.FAILED_TASKS == self.FAILED_TASKS:
|
||||
state.run_state = self.ITERATING_RESCUE
|
||||
elif state.cur_regular_task >= len(block.block):
|
||||
state.run_state = self.ITERATING_ALWAYS
|
||||
else:
|
||||
task = block.block[state.cur_regular_task]
|
||||
state.cur_regular_task += 1
|
||||
|
||||
elif state.run_state == self.ITERATING_RESCUE:
|
||||
if state.fail_state & self.FAILED_RESCUE == self.FAILED_RESCUE:
|
||||
state.run_state = self.ITERATING_ALWAYS
|
||||
elif state.cur_rescue_task >= len(block.rescue):
|
||||
if len(block.rescue) > 0:
|
||||
state.fail_state = self.FAILED_NONE
|
||||
state.run_state = self.ITERATING_ALWAYS
|
||||
else:
|
||||
task = block.rescue[state.cur_rescue_task]
|
||||
state.cur_rescue_task += 1
|
||||
|
||||
elif state.run_state == self.ITERATING_ALWAYS:
|
||||
if state.cur_always_task >= len(block.always):
|
||||
if state.fail_state != self.FAILED_NONE:
|
||||
state.run_state = self.ITERATING_COMPLETE
|
||||
else:
|
||||
state.cur_block += 1
|
||||
state.cur_regular_task = 0
|
||||
state.cur_rescue_task = 0
|
||||
state.cur_always_task = 0
|
||||
state.run_state = self.ITERATING_TASKS
|
||||
state.child_state = None
|
||||
else:
|
||||
task = block.always[state.cur_always_task]
|
||||
state.cur_always_task += 1
|
||||
|
||||
elif state.run_state == self.ITERATING_COMPLETE:
|
||||
return (state, None)
|
||||
|
||||
# if the current task is actually a child block, we dive into it
|
||||
if isinstance(task, Block):
|
||||
state.child_state = HostState(blocks=[task])
|
||||
state.child_state.run_state = self.ITERATING_TASKS
|
||||
state.child_state.cur_role = state.cur_role
|
||||
(state.child_state, task) = self._get_next_task_from_state(state.child_state, peek=peek)
|
||||
|
||||
# if something above set the task, break out of the loop now
|
||||
if task:
|
||||
break
|
||||
|
||||
return (state, task)
|
||||
|
||||
def mark_host_failed(self, host):
|
||||
s = self.get_host_state(host)
|
||||
if s.pending_setup:
|
||||
s.fail_state |= self.FAILED_SETUP
|
||||
s.run_state = self.ITERATING_COMPLETE
|
||||
elif s.run_state == self.ITERATING_TASKS:
|
||||
s.fail_state |= self.FAILED_TASKS
|
||||
s.run_state = self.ITERATING_RESCUE
|
||||
elif s.run_state == self.ITERATING_RESCUE:
|
||||
s.fail_state |= self.FAILED_RESCUE
|
||||
s.run_state = self.ITERATING_ALWAYS
|
||||
elif s.run_state == self.ITERATING_ALWAYS:
|
||||
s.fail_state |= self.FAILED_ALWAYS
|
||||
s.run_state = self.ITERATING_COMPLETE
|
||||
self._host_states[host.name] = s
|
||||
|
||||
def get_failed_hosts(self):
|
||||
return dict((host, True) for (host, state) in self._host_states.iteritems() if state.run_state == self.ITERATING_COMPLETE and state.fail_state != self.FAILED_NONE)
|
||||
|
||||
def get_original_task(self, host, task):
|
||||
'''
|
||||
Finds the task in the task list which matches the UUID of the given task.
|
||||
The executor engine serializes/deserializes objects as they are passed through
|
||||
the different processes, and not all data structures are preserved. This method
|
||||
allows us to find the original task passed into the executor engine.
|
||||
'''
|
||||
def _search_block(block, task):
|
||||
for t in block.block:
|
||||
if isinstance(t, Block):
|
||||
res = _search_block(t, task)
|
||||
if res:
|
||||
return res
|
||||
elif t._uuid == task._uuid:
|
||||
return t
|
||||
for t in block.rescue:
|
||||
if isinstance(t, Block):
|
||||
res = _search_block(t, task)
|
||||
if res:
|
||||
return res
|
||||
elif t._uuid == task._uuid:
|
||||
return t
|
||||
for t in block.always:
|
||||
if isinstance(t, Block):
|
||||
res = _search_block(t, task)
|
||||
if res:
|
||||
return res
|
||||
elif t._uuid == task._uuid:
|
||||
return t
|
||||
return None
|
||||
|
||||
s = self.get_host_state(host)
|
||||
for block in s._blocks:
|
||||
res = _search_block(block, task)
|
||||
if res:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def add_tasks(self, host, task_list):
|
||||
s = self.get_host_state(host)
|
||||
target_block = s._blocks[s.cur_block].copy(exclude_parent=True)
|
||||
|
||||
if s.run_state == self.ITERATING_TASKS:
|
||||
before = target_block.block[:s.cur_regular_task]
|
||||
after = target_block.block[s.cur_regular_task:]
|
||||
target_block.block = before + task_list + after
|
||||
elif s.run_state == self.ITERATING_RESCUE:
|
||||
before = target_block.rescue[:s.cur_rescue_task]
|
||||
after = target_block.rescue[s.cur_rescue_task:]
|
||||
target_block.rescue = before + task_list + after
|
||||
elif s.run_state == self.ITERATING_ALWAYS:
|
||||
before = target_block.always[:s.cur_always_task]
|
||||
after = target_block.always[s.cur_always_task:]
|
||||
target_block.always = before + task_list + after
|
||||
|
||||
s._blocks[s.cur_block] = target_block
|
||||
self._host_states[host.name] = s
|
||||
|
||||
224
lib/ansible/executor/playbook_executor.py
Normal file
224
lib/ansible/executor/playbook_executor.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# (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 signal
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import *
|
||||
from ansible.executor.task_queue_manager import TaskQueueManager
|
||||
from ansible.playbook import Playbook
|
||||
from ansible.template import Templar
|
||||
|
||||
from ansible.utils.color import colorize, hostcolor
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
class PlaybookExecutor:
|
||||
|
||||
'''
|
||||
This is the primary class for executing playbooks, and thus the
|
||||
basis for bin/ansible-playbook operation.
|
||||
'''
|
||||
|
||||
def __init__(self, playbooks, inventory, variable_manager, loader, display, options, passwords):
|
||||
self._playbooks = playbooks
|
||||
self._inventory = inventory
|
||||
self._variable_manager = variable_manager
|
||||
self._loader = loader
|
||||
self._display = display
|
||||
self._options = options
|
||||
self.passwords = passwords
|
||||
|
||||
if options.listhosts or options.listtasks or options.listtags or options.syntax:
|
||||
self._tqm = None
|
||||
else:
|
||||
self._tqm = TaskQueueManager(inventory=inventory, variable_manager=variable_manager, loader=loader, display=display, options=options, passwords=self.passwords)
|
||||
|
||||
def run(self):
|
||||
|
||||
'''
|
||||
Run the given playbook, based on the settings in the play which
|
||||
may limit the runs to serialized groups, etc.
|
||||
'''
|
||||
|
||||
signal.signal(signal.SIGINT, self._cleanup)
|
||||
|
||||
result = 0
|
||||
entrylist = []
|
||||
entry = {}
|
||||
try:
|
||||
for playbook_path in self._playbooks:
|
||||
pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)
|
||||
|
||||
if self._tqm is None: # we are doing a listing
|
||||
entry = {'playbook': playbook_path}
|
||||
entry['plays'] = []
|
||||
|
||||
i = 1
|
||||
plays = pb.get_plays()
|
||||
self._display.vv('%d plays in %s' % (len(plays), playbook_path))
|
||||
|
||||
for play in plays:
|
||||
self._inventory.remove_restriction()
|
||||
|
||||
# Create a temporary copy of the play here, so we can run post_validate
|
||||
# on it without the templating changes affecting the original object.
|
||||
all_vars = self._variable_manager.get_vars(loader=self._loader, play=play)
|
||||
templar = Templar(loader=self._loader, variables=all_vars, fail_on_undefined=False)
|
||||
new_play = play.copy()
|
||||
new_play.post_validate(templar)
|
||||
|
||||
if self._options.syntax:
|
||||
continue
|
||||
|
||||
if self._tqm is None:
|
||||
# we are just doing a listing
|
||||
|
||||
pname = new_play.get_name().strip()
|
||||
if pname == 'PLAY: <no name specified>':
|
||||
pname = 'PLAY: #%d' % i
|
||||
p = { 'name': pname }
|
||||
|
||||
if self._options.listhosts:
|
||||
p['pattern']=play.hosts
|
||||
p['hosts']=set(self._inventory.get_hosts(new_play.hosts))
|
||||
|
||||
#TODO: play tasks are really blocks, need to figure out how to get task objects from them
|
||||
elif self._options.listtasks:
|
||||
p['tasks'] = []
|
||||
for task in play.get_tasks():
|
||||
p['tasks'].append(task)
|
||||
#p['tasks'].append({'name': task.get_name().strip(), 'tags': task.tags})
|
||||
|
||||
elif self._options.listtags:
|
||||
p['tags'] = set(new_play.tags)
|
||||
for task in play.get_tasks():
|
||||
p['tags'].update(task)
|
||||
#p['tags'].update(task.tags)
|
||||
entry['plays'].append(p)
|
||||
|
||||
else:
|
||||
# we are actually running plays
|
||||
for batch in self._get_serialized_batches(new_play):
|
||||
if len(batch) == 0:
|
||||
self._tqm.send_callback('v2_playbook_on_play_start', new_play)
|
||||
self._tqm.send_callback('v2_playbook_on_no_hosts_matched')
|
||||
result = 1
|
||||
break
|
||||
# restrict the inventory to the hosts in the serialized batch
|
||||
self._inventory.restrict_to_hosts(batch)
|
||||
# and run it...
|
||||
result = self._tqm.run(play=play)
|
||||
# if the last result wasn't zero, break out of the serial batch loop
|
||||
if result != 0:
|
||||
break
|
||||
|
||||
# if the last result wasn't zero, break out of the play loop
|
||||
if result != 0:
|
||||
break
|
||||
|
||||
i = i + 1 # per play
|
||||
|
||||
if entry:
|
||||
entrylist.append(entry) # per playbook
|
||||
|
||||
# if the last result wasn't zero, break out of the playbook file name loop
|
||||
if result != 0:
|
||||
break
|
||||
|
||||
if entrylist:
|
||||
return entrylist
|
||||
|
||||
finally:
|
||||
if self._tqm is not None:
|
||||
self._cleanup()
|
||||
|
||||
if self._options.syntax:
|
||||
self.display.display("No issues encountered")
|
||||
return result
|
||||
|
||||
# FIXME: this stat summary stuff should be cleaned up and moved
|
||||
# to a new method, if it even belongs here...
|
||||
self._display.banner("PLAY RECAP")
|
||||
|
||||
hosts = sorted(self._tqm._stats.processed.keys())
|
||||
for h in hosts:
|
||||
t = self._tqm._stats.summarize(h)
|
||||
|
||||
self._display.display("%s : %s %s %s %s" % (
|
||||
hostcolor(h, t),
|
||||
colorize('ok', t['ok'], 'green'),
|
||||
colorize('changed', t['changed'], 'yellow'),
|
||||
colorize('unreachable', t['unreachable'], 'red'),
|
||||
colorize('failed', t['failures'], 'red')),
|
||||
screen_only=True
|
||||
)
|
||||
|
||||
self._display.display("%s : %s %s %s %s" % (
|
||||
hostcolor(h, t, False),
|
||||
colorize('ok', t['ok'], None),
|
||||
colorize('changed', t['changed'], None),
|
||||
colorize('unreachable', t['unreachable'], None),
|
||||
colorize('failed', t['failures'], None)),
|
||||
log_only=True
|
||||
)
|
||||
|
||||
self._display.display("", screen_only=True)
|
||||
# END STATS STUFF
|
||||
|
||||
return result
|
||||
|
||||
def _cleanup(self, signum=None, framenum=None):
|
||||
return self._tqm.cleanup()
|
||||
|
||||
def _get_serialized_batches(self, play):
|
||||
'''
|
||||
Returns a list of hosts, subdivided into batches based on
|
||||
the serial size specified in the play.
|
||||
'''
|
||||
|
||||
# make sure we have a unique list of hosts
|
||||
all_hosts = self._inventory.get_hosts(play.hosts)
|
||||
|
||||
# check to see if the serial number was specified as a percentage,
|
||||
# and convert it to an integer value based on the number of hosts
|
||||
if isinstance(play.serial, basestring) and play.serial.endswith('%'):
|
||||
serial_pct = int(play.serial.replace("%",""))
|
||||
serial = int((serial_pct/100.0) * len(all_hosts))
|
||||
else:
|
||||
serial = int(play.serial)
|
||||
|
||||
# if the serial count was not specified or is invalid, default to
|
||||
# a list of all hosts, otherwise split the list of hosts into chunks
|
||||
# which are based on the serial size
|
||||
if serial <= 0:
|
||||
return [all_hosts]
|
||||
else:
|
||||
serialized_batches = []
|
||||
|
||||
while len(all_hosts) > 0:
|
||||
play_hosts = []
|
||||
for x in range(serial):
|
||||
if len(all_hosts) > 0:
|
||||
play_hosts.append(all_hosts.pop(0))
|
||||
|
||||
serialized_batches.append(play_hosts)
|
||||
|
||||
return serialized_batches
|
||||
21
lib/ansible/executor/process/__init__.py
Normal file
21
lib/ansible/executor/process/__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
|
||||
|
||||
176
lib/ansible/executor/process/result.py
Normal file
176
lib/ansible/executor/process/result.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# (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
|
||||
import multiprocessing
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
HAS_ATFORK=True
|
||||
try:
|
||||
from Crypto.Random import atfork
|
||||
except ImportError:
|
||||
HAS_ATFORK=False
|
||||
|
||||
from ansible.playbook.handler import Handler
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
__all__ = ['ResultProcess']
|
||||
|
||||
|
||||
class ResultProcess(multiprocessing.Process):
|
||||
'''
|
||||
The result worker thread, which reads results from the results
|
||||
queue and fires off callbacks/etc. as necessary.
|
||||
'''
|
||||
|
||||
def __init__(self, final_q, workers):
|
||||
|
||||
# takes a task queue manager as the sole param:
|
||||
self._final_q = final_q
|
||||
self._workers = workers
|
||||
self._cur_worker = 0
|
||||
self._terminated = False
|
||||
|
||||
super(ResultProcess, self).__init__()
|
||||
|
||||
def _send_result(self, result):
|
||||
debug("sending result: %s" % (result,))
|
||||
self._final_q.put(result, block=False)
|
||||
debug("done sending result")
|
||||
|
||||
def _read_worker_result(self):
|
||||
result = None
|
||||
starting_point = self._cur_worker
|
||||
while True:
|
||||
(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
|
||||
|
||||
try:
|
||||
if not rslt_q.empty():
|
||||
debug("worker %d has data to read" % self._cur_worker)
|
||||
result = rslt_q.get(block=False)
|
||||
debug("got a result from worker %d: %s" % (self._cur_worker, result))
|
||||
break
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
if self._cur_worker == starting_point:
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def terminate(self):
|
||||
self._terminated = True
|
||||
super(ResultProcess, self).terminate()
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
The main thread execution, which reads from the results queue
|
||||
indefinitely and sends callbacks/etc. when results are received.
|
||||
'''
|
||||
|
||||
if HAS_ATFORK:
|
||||
atfork()
|
||||
|
||||
while True:
|
||||
try:
|
||||
result = self._read_worker_result()
|
||||
if result is None:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
host_name = result._host.get_name()
|
||||
|
||||
# send callbacks, execute other options based on the result status
|
||||
# FIXME: this should all be cleaned up and probably moved to a sub-function.
|
||||
# the fact that this sometimes sends a TaskResult and other times
|
||||
# sends a raw dictionary back may be confusing, but the result vs.
|
||||
# results implementation for tasks with loops should be cleaned up
|
||||
# better than this
|
||||
if result.is_unreachable():
|
||||
self._send_result(('host_unreachable', result))
|
||||
elif result.is_failed():
|
||||
self._send_result(('host_task_failed', result))
|
||||
elif result.is_skipped():
|
||||
self._send_result(('host_task_skipped', result))
|
||||
else:
|
||||
# if this task is notifying a handler, do it now
|
||||
if result._task.notify:
|
||||
# The shared dictionary for notified handlers is a proxy, which
|
||||
# does not detect when sub-objects within the proxy are modified.
|
||||
# So, per the docs, we reassign the list so the proxy picks up and
|
||||
# notifies all other threads
|
||||
for notify in result._task.notify:
|
||||
self._send_result(('notify_handler', result._host, notify))
|
||||
|
||||
if result._task.loop:
|
||||
# this task had a loop, and has more than one result, so
|
||||
# loop over all of them instead of a single result
|
||||
result_items = result._result['results']
|
||||
else:
|
||||
result_items = [ result._result ]
|
||||
|
||||
for result_item in result_items:
|
||||
#if 'include' in result_item:
|
||||
# include_variables = result_item.get('include_variables', dict())
|
||||
# if 'item' in result_item:
|
||||
# include_variables['item'] = result_item['item']
|
||||
# self._send_result(('include', result._host, result._task, result_item['include'], include_variables))
|
||||
#elif 'add_host' in result_item:
|
||||
if 'add_host' in result_item:
|
||||
# this task added a new host (add_host module)
|
||||
self._send_result(('add_host', result_item))
|
||||
elif 'add_group' in result_item:
|
||||
# this task added a new group (group_by module)
|
||||
self._send_result(('add_group', result._host, result_item))
|
||||
elif 'ansible_facts' in result_item:
|
||||
# if this task is registering facts, do that now
|
||||
if result._task.action in ('set_fact', 'include_vars'):
|
||||
for (key, value) in result_item['ansible_facts'].iteritems():
|
||||
self._send_result(('set_host_var', result._host, key, value))
|
||||
else:
|
||||
self._send_result(('set_host_facts', result._host, result_item['ansible_facts']))
|
||||
|
||||
# finally, send the ok for this task
|
||||
self._send_result(('host_task_ok', result))
|
||||
|
||||
# if this task is registering a result, do it now
|
||||
if result._task.register:
|
||||
self._send_result(('set_host_var', result._host, result._task.register, result._result))
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
except (KeyboardInterrupt, IOError, EOFError):
|
||||
break
|
||||
except:
|
||||
# FIXME: we should probably send a proper callback here instead of
|
||||
# simply dumping a stack trace on the screen
|
||||
traceback.print_exc()
|
||||
break
|
||||
|
||||
155
lib/ansible/executor/process/worker.py
Normal file
155
lib/ansible/executor/process/worker.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# (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
|
||||
import multiprocessing
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
HAS_ATFORK=True
|
||||
try:
|
||||
from Crypto.Random import atfork
|
||||
except ImportError:
|
||||
HAS_ATFORK=False
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||||
from ansible.executor.task_executor import TaskExecutor
|
||||
from ansible.executor.task_result import TaskResult
|
||||
from ansible.playbook.handler import Handler
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
__all__ = ['WorkerProcess']
|
||||
|
||||
|
||||
class WorkerProcess(multiprocessing.Process):
|
||||
'''
|
||||
The worker thread class, which uses TaskExecutor to run tasks
|
||||
read from a job queue and pushes results into a results queue
|
||||
for reading later.
|
||||
'''
|
||||
|
||||
def __init__(self, tqm, main_q, rslt_q, loader):
|
||||
|
||||
# takes a task queue manager as the sole param:
|
||||
self._main_q = main_q
|
||||
self._rslt_q = rslt_q
|
||||
self._loader = loader
|
||||
|
||||
# dupe stdin, if we have one
|
||||
self._new_stdin = sys.stdin
|
||||
try:
|
||||
fileno = sys.stdin.fileno()
|
||||
if fileno is not None:
|
||||
try:
|
||||
self._new_stdin = os.fdopen(os.dup(fileno))
|
||||
except OSError, e:
|
||||
# couldn't dupe stdin, most likely because it's
|
||||
# not a valid file descriptor, so we just rely on
|
||||
# using the one that was passed in
|
||||
pass
|
||||
except ValueError:
|
||||
# couldn't get stdin's fileno, so we just carry on
|
||||
pass
|
||||
|
||||
super(WorkerProcess, self).__init__()
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
Called when the process is started, and loops indefinitely
|
||||
until an error is encountered (typically an IOerror from the
|
||||
queue pipe being disconnected). During the loop, we attempt
|
||||
to pull tasks off the job queue and run them, pushing the result
|
||||
onto the results queue. We also remove the host from the blocked
|
||||
hosts list, to signify that they are ready for their next task.
|
||||
'''
|
||||
|
||||
if HAS_ATFORK:
|
||||
atfork()
|
||||
|
||||
while True:
|
||||
task = None
|
||||
try:
|
||||
if not self._main_q.empty():
|
||||
debug("there's work to be done!")
|
||||
(host, task, basedir, job_vars, connection_info, shared_loader_obj) = self._main_q.get(block=False)
|
||||
debug("got a task/handler to work on: %s" % task)
|
||||
|
||||
# because the task queue manager starts workers (forks) before the
|
||||
# playbook is loaded, set the basedir of the loader inherted by
|
||||
# this fork now so that we can find files correctly
|
||||
self._loader.set_basedir(basedir)
|
||||
|
||||
# Serializing/deserializing tasks does not preserve the loader attribute,
|
||||
# since it is passed to the worker during the forking of the process and
|
||||
# would be wasteful to serialize. So we set it here on the task now, and
|
||||
# the task handles updating parent/child objects as needed.
|
||||
task.set_loader(self._loader)
|
||||
|
||||
# apply the given task's information to the connection info,
|
||||
# which may override some fields already set by the play or
|
||||
# the options specified on the command line
|
||||
new_connection_info = connection_info.set_task_and_host_override(task=task, host=host)
|
||||
|
||||
# execute the task and build a TaskResult from the result
|
||||
debug("running TaskExecutor() for %s/%s" % (host, task))
|
||||
executor_result = TaskExecutor(host, task, job_vars, new_connection_info, self._new_stdin, self._loader, shared_loader_obj).run()
|
||||
debug("done running TaskExecutor() for %s/%s" % (host, task))
|
||||
task_result = TaskResult(host, task, executor_result)
|
||||
|
||||
# put the result on the result queue
|
||||
debug("sending task result")
|
||||
self._rslt_q.put(task_result, block=False)
|
||||
debug("done sending task result")
|
||||
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
except (IOError, EOFError, KeyboardInterrupt):
|
||||
break
|
||||
except AnsibleConnectionFailure:
|
||||
try:
|
||||
if task:
|
||||
task_result = TaskResult(host, task, dict(unreachable=True))
|
||||
self._rslt_q.put(task_result, block=False)
|
||||
except:
|
||||
# FIXME: most likely an abort, catch those kinds of errors specifically
|
||||
break
|
||||
except Exception, e:
|
||||
debug("WORKER EXCEPTION: %s" % e)
|
||||
debug("WORKER EXCEPTION: %s" % traceback.format_exc())
|
||||
try:
|
||||
if task:
|
||||
task_result = TaskResult(host, task, dict(failed=True, exception=traceback.format_exc(), stdout=''))
|
||||
self._rslt_q.put(task_result, block=False)
|
||||
except:
|
||||
# FIXME: most likely an abort, catch those kinds of errors specifically
|
||||
break
|
||||
|
||||
debug("WORKER PROCESS EXITING")
|
||||
|
||||
|
||||
51
lib/ansible/executor/stats.py
Normal file
51
lib/ansible/executor/stats.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# (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
|
||||
|
||||
class AggregateStats:
|
||||
''' holds stats about per-host activity during playbook runs '''
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.processed = {}
|
||||
self.failures = {}
|
||||
self.ok = {}
|
||||
self.dark = {}
|
||||
self.changed = {}
|
||||
self.skipped = {}
|
||||
|
||||
def increment(self, what, host):
|
||||
''' helper function to bump a statistic '''
|
||||
|
||||
self.processed[host] = 1
|
||||
prev = (getattr(self, what)).get(host, 0)
|
||||
getattr(self, what)[host] = prev+1
|
||||
|
||||
def summarize(self, host):
|
||||
''' return information about a particular host '''
|
||||
|
||||
return dict(
|
||||
ok = self.ok.get(host, 0),
|
||||
failures = self.failures.get(host, 0),
|
||||
unreachable = self.dark.get(host,0),
|
||||
changed = self.changed.get(host, 0),
|
||||
skipped = self.skipped.get(host, 0)
|
||||
)
|
||||
|
||||
460
lib/ansible/executor/task_executor.py
Normal file
460
lib/ansible/executor/task_executor.py
Normal file
@@ -0,0 +1,460 @@
|
||||
# (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
|
||||
import pipes
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.executor.connection_info import ConnectionInformation
|
||||
from ansible.playbook.conditional import Conditional
|
||||
from ansible.playbook.task import Task
|
||||
from ansible.plugins import lookup_loader, connection_loader, action_loader
|
||||
from ansible.template import Templar
|
||||
from ansible.utils.listify import listify_lookup_plugin_terms
|
||||
from ansible.utils.unicode import to_unicode
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
__all__ = ['TaskExecutor']
|
||||
|
||||
class TaskExecutor:
|
||||
|
||||
'''
|
||||
This is the main worker class for the executor pipeline, which
|
||||
handles loading an action plugin to actually dispatch the task to
|
||||
a given host. This class roughly corresponds to the old Runner()
|
||||
class.
|
||||
'''
|
||||
|
||||
def __init__(self, host, task, job_vars, connection_info, new_stdin, loader, shared_loader_obj):
|
||||
self._host = host
|
||||
self._task = task
|
||||
self._job_vars = job_vars
|
||||
self._connection_info = connection_info
|
||||
self._new_stdin = new_stdin
|
||||
self._loader = loader
|
||||
self._shared_loader_obj = shared_loader_obj
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
The main executor entrypoint, where we determine if the specified
|
||||
task requires looping and either runs the task with
|
||||
'''
|
||||
|
||||
debug("in run()")
|
||||
|
||||
try:
|
||||
# lookup plugins need to know if this task is executing from
|
||||
# a role, so that it can properly find files/templates/etc.
|
||||
roledir = None
|
||||
if self._task._role:
|
||||
roledir = self._task._role._role_path
|
||||
self._job_vars['roledir'] = roledir
|
||||
|
||||
items = self._get_loop_items()
|
||||
if items is not None:
|
||||
if len(items) > 0:
|
||||
item_results = self._run_loop(items)
|
||||
|
||||
# loop through the item results, and remember the changed/failed
|
||||
# result flags based on any item there.
|
||||
changed = False
|
||||
failed = False
|
||||
for item in item_results:
|
||||
if 'changed' in item:
|
||||
changed = True
|
||||
if 'failed' in item:
|
||||
failed = True
|
||||
|
||||
# create the overall result item, and set the changed/failed
|
||||
# flags there to reflect the overall result of the loop
|
||||
res = dict(results=item_results)
|
||||
|
||||
if changed:
|
||||
res['changed'] = True
|
||||
|
||||
if failed:
|
||||
res['failed'] = True
|
||||
res['msg'] = 'One or more items failed'
|
||||
else:
|
||||
res['msg'] = 'All items completed'
|
||||
else:
|
||||
res = dict(changed=False, skipped=True, skipped_reason='No items in the list', results=[])
|
||||
else:
|
||||
debug("calling self._execute()")
|
||||
res = self._execute()
|
||||
debug("_execute() done")
|
||||
|
||||
# make sure changed is set in the result, if it's not present
|
||||
if 'changed' not in res:
|
||||
res['changed'] = False
|
||||
|
||||
debug("dumping result to json")
|
||||
result = json.dumps(res)
|
||||
debug("done dumping result, returning")
|
||||
return result
|
||||
except AnsibleError, e:
|
||||
return dict(failed=True, msg=to_unicode(e, nonstring='simplerepr'))
|
||||
|
||||
def _get_loop_items(self):
|
||||
'''
|
||||
Loads a lookup plugin to handle the with_* portion of a task (if specified),
|
||||
and returns the items result.
|
||||
'''
|
||||
|
||||
items = None
|
||||
if self._task.loop and self._task.loop in lookup_loader:
|
||||
loop_terms = listify_lookup_plugin_terms(terms=self._task.loop_args, variables=self._job_vars, loader=self._loader)
|
||||
items = lookup_loader.get(self._task.loop, loader=self._loader).run(terms=loop_terms, variables=self._job_vars)
|
||||
|
||||
return items
|
||||
|
||||
def _run_loop(self, items):
|
||||
'''
|
||||
Runs the task with the loop items specified and collates the result
|
||||
into an array named 'results' which is inserted into the final result
|
||||
along with the item for which the loop ran.
|
||||
'''
|
||||
|
||||
results = []
|
||||
|
||||
# make copies of the job vars and task so we can add the item to
|
||||
# the variables and re-validate the task with the item variable
|
||||
task_vars = self._job_vars.copy()
|
||||
|
||||
items = self._squash_items(items, task_vars)
|
||||
for item in items:
|
||||
task_vars['item'] = item
|
||||
|
||||
try:
|
||||
tmp_task = self._task.copy()
|
||||
except AnsibleParserError, e:
|
||||
results.append(dict(failed=True, msg=str(e)))
|
||||
continue
|
||||
|
||||
# now we swap the internal task with the copy, execute,
|
||||
# and swap them back so we can do the next iteration cleanly
|
||||
(self._task, tmp_task) = (tmp_task, self._task)
|
||||
res = self._execute(variables=task_vars)
|
||||
(self._task, tmp_task) = (tmp_task, self._task)
|
||||
|
||||
# now update the result with the item info, and append the result
|
||||
# to the list of results
|
||||
res['item'] = item
|
||||
results.append(res)
|
||||
|
||||
# FIXME: we should be sending back a callback result for each item in the loop here
|
||||
print(res)
|
||||
|
||||
return results
|
||||
|
||||
def _squash_items(self, items, variables):
|
||||
'''
|
||||
Squash items down to a comma-separated list for certain modules which support it
|
||||
(typically package management modules).
|
||||
'''
|
||||
|
||||
if len(items) > 0 and self._task.action in ('apt', 'yum', 'pkgng', 'zypper'):
|
||||
final_items = []
|
||||
for item in items:
|
||||
variables['item'] = item
|
||||
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables)
|
||||
if self._task.evaluate_conditional(templar, variables):
|
||||
final_items.append(item)
|
||||
return [",".join(final_items)]
|
||||
else:
|
||||
return items
|
||||
|
||||
def _execute(self, variables=None):
|
||||
'''
|
||||
The primary workhorse of the executor system, this runs the task
|
||||
on the specified host (which may be the delegated_to host) and handles
|
||||
the retry/until and block rescue/always execution
|
||||
'''
|
||||
|
||||
if variables is None:
|
||||
variables = self._job_vars
|
||||
|
||||
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables)
|
||||
|
||||
# fields set from the play/task may be based on variables, so we have to
|
||||
# do the same kind of post validation step on it here before we use it.
|
||||
self._connection_info.post_validate(templar=templar)
|
||||
|
||||
# now that the connection information is finalized, we can add 'magic'
|
||||
# variables to the variable dictionary
|
||||
self._connection_info.update_vars(variables)
|
||||
|
||||
# get the connection and the handler for this execution
|
||||
self._connection = self._get_connection(variables)
|
||||
self._connection.set_host_overrides(host=self._host)
|
||||
self._connection._connect()
|
||||
|
||||
self._handler = self._get_action_handler(connection=self._connection, templar=templar)
|
||||
|
||||
# Evaluate the conditional (if any) for this task, which we do before running
|
||||
# the final task post-validation. We do this before the post validation due to
|
||||
# the fact that the conditional may specify that the task be skipped due to a
|
||||
# variable not being present which would otherwise cause validation to fail
|
||||
if not self._task.evaluate_conditional(templar, variables):
|
||||
debug("when evaulation failed, skipping this task")
|
||||
return dict(changed=False, skipped=True, skip_reason='Conditional check failed')
|
||||
|
||||
# Now we do final validation on the task, which sets all fields to their final values
|
||||
self._task.post_validate(templar=templar)
|
||||
|
||||
# if this task is a TaskInclude, we just return now with a success code so the
|
||||
# main thread can expand the task list for the given host
|
||||
if self._task.action == 'include':
|
||||
include_variables = self._task.args.copy()
|
||||
include_file = include_variables.get('_raw_params')
|
||||
del include_variables['_raw_params']
|
||||
return dict(changed=True, include=include_file, include_variables=include_variables)
|
||||
|
||||
# And filter out any fields which were set to default(omit), and got the omit token value
|
||||
omit_token = variables.get('omit')
|
||||
if omit_token is not None:
|
||||
self._task.args = dict(filter(lambda x: x[1] != omit_token, self._task.args.iteritems()))
|
||||
|
||||
# Read some values from the task, so that we can modify them if need be
|
||||
retries = self._task.retries
|
||||
if retries <= 0:
|
||||
retries = 1
|
||||
|
||||
delay = self._task.delay
|
||||
if delay < 0:
|
||||
delay = 1
|
||||
|
||||
# make a copy of the job vars here, in case we need to update them
|
||||
# with the registered variable value later on when testing conditions
|
||||
vars_copy = variables.copy()
|
||||
|
||||
debug("starting attempt loop")
|
||||
result = None
|
||||
for attempt in range(retries):
|
||||
if attempt > 0:
|
||||
# FIXME: this should use the callback/message passing mechanism
|
||||
print("FAILED - RETRYING: %s (%d retries left)" % (self._task, retries-attempt))
|
||||
result['attempts'] = attempt + 1
|
||||
|
||||
debug("running the handler")
|
||||
result = self._handler.run(task_vars=variables)
|
||||
debug("handler run complete")
|
||||
|
||||
if self._task.async > 0:
|
||||
# the async_wrapper module returns dumped JSON via its stdout
|
||||
# response, so we parse it here and replace the result
|
||||
try:
|
||||
result = json.loads(result.get('stdout'))
|
||||
except ValueError, e:
|
||||
return dict(failed=True, msg="The async task did not return valid JSON: %s" % str(e))
|
||||
|
||||
if self._task.poll > 0:
|
||||
result = self._poll_async_result(result=result, templar=templar)
|
||||
|
||||
# update the local copy of vars with the registered value, if specified,
|
||||
# or any facts which may have been generated by the module execution
|
||||
if self._task.register:
|
||||
vars_copy[self._task.register] = result
|
||||
|
||||
if 'ansible_facts' in result:
|
||||
vars_copy.update(result['ansible_facts'])
|
||||
|
||||
# create a conditional object to evaluate task conditions
|
||||
cond = Conditional(loader=self._loader)
|
||||
|
||||
# FIXME: make sure until is mutually exclusive with changed_when/failed_when
|
||||
if self._task.until:
|
||||
cond.when = self._task.until
|
||||
if cond.evaluate_conditional(templar, vars_copy):
|
||||
break
|
||||
elif (self._task.changed_when or self._task.failed_when) and 'skipped' not in result:
|
||||
if self._task.changed_when:
|
||||
cond.when = [ self._task.changed_when ]
|
||||
result['changed'] = cond.evaluate_conditional(templar, vars_copy)
|
||||
if self._task.failed_when:
|
||||
cond.when = [ self._task.failed_when ]
|
||||
failed_when_result = cond.evaluate_conditional(templar, vars_copy)
|
||||
result['failed_when_result'] = result['failed'] = failed_when_result
|
||||
if failed_when_result:
|
||||
break
|
||||
elif 'failed' not in result and result.get('rc', 0) == 0:
|
||||
# if the result is not failed, stop trying
|
||||
break
|
||||
|
||||
if attempt < retries - 1:
|
||||
time.sleep(delay)
|
||||
|
||||
# do the final update of the local variables here, for both registered
|
||||
# values and any facts which may have been created
|
||||
if self._task.register:
|
||||
variables[self._task.register] = result
|
||||
|
||||
if 'ansible_facts' in result:
|
||||
variables.update(result['ansible_facts'])
|
||||
|
||||
# and return
|
||||
debug("attempt loop complete, returning result")
|
||||
return result
|
||||
|
||||
def _poll_async_result(self, result, templar):
|
||||
'''
|
||||
Polls for the specified JID to be complete
|
||||
'''
|
||||
|
||||
async_jid = result.get('ansible_job_id')
|
||||
if async_jid is None:
|
||||
return dict(failed=True, msg="No job id was returned by the async task")
|
||||
|
||||
# Create a new psuedo-task to run the async_status module, and run
|
||||
# that (with a sleep for "poll" seconds between each retry) until the
|
||||
# async time limit is exceeded.
|
||||
|
||||
async_task = Task().load(dict(action='async_status jid=%s' % async_jid))
|
||||
|
||||
# Because this is an async task, the action handler is async. However,
|
||||
# we need the 'normal' action handler for the status check, so get it
|
||||
# now via the action_loader
|
||||
normal_handler = action_loader.get(
|
||||
'normal',
|
||||
task=async_task,
|
||||
connection=self._connection,
|
||||
connection_info=self._connection_info,
|
||||
loader=self._loader,
|
||||
templar=templar,
|
||||
shared_loader_obj=self._shared_loader_obj,
|
||||
)
|
||||
|
||||
time_left = self._task.async
|
||||
while time_left > 0:
|
||||
time.sleep(self._task.poll)
|
||||
|
||||
async_result = normal_handler.run()
|
||||
if int(async_result.get('finished', 0)) == 1 or 'failed' in async_result or 'skipped' in async_result:
|
||||
break
|
||||
|
||||
time_left -= self._task.poll
|
||||
|
||||
if int(async_result.get('finished', 0)) != 1:
|
||||
return dict(failed=True, msg="async task did not complete within the requested time")
|
||||
else:
|
||||
return async_result
|
||||
|
||||
def _get_connection(self, variables):
|
||||
'''
|
||||
Reads the connection property for the host, and returns the
|
||||
correct connection object from the list of connection plugins
|
||||
'''
|
||||
|
||||
# FIXME: delegate_to calculation should be done here
|
||||
# FIXME: calculation of connection params/auth stuff should be done here
|
||||
|
||||
self._connection_info.remote_addr = self._host.ipv4_address
|
||||
if self._task.delegate_to is not None:
|
||||
self._compute_delegate(variables)
|
||||
|
||||
conn_type = self._connection_info.connection
|
||||
if conn_type == 'smart':
|
||||
conn_type = 'ssh'
|
||||
if sys.platform.startswith('darwin') and self._connection_info.remote_pass:
|
||||
# due to a current bug in sshpass on OSX, which can trigger
|
||||
# a kernel panic even for non-privileged users, we revert to
|
||||
# paramiko on that OS when a SSH password is specified
|
||||
conn_type = "paramiko"
|
||||
else:
|
||||
# see if SSH can support ControlPersist if not use paramiko
|
||||
cmd = subprocess.Popen(['ssh','-o','ControlPersist'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
if "Bad configuration option" in err:
|
||||
conn_type = "paramiko"
|
||||
|
||||
connection = connection_loader.get(conn_type, self._connection_info, self._new_stdin)
|
||||
if not connection:
|
||||
raise AnsibleError("the connection plugin '%s' was not found" % conn_type)
|
||||
|
||||
return connection
|
||||
|
||||
def _get_action_handler(self, connection, templar):
|
||||
'''
|
||||
Returns the correct action plugin to handle the requestion task action
|
||||
'''
|
||||
|
||||
if self._task.action in action_loader:
|
||||
if self._task.async != 0:
|
||||
raise AnsibleError("async mode is not supported with the %s module" % module_name)
|
||||
handler_name = self._task.action
|
||||
elif self._task.async == 0:
|
||||
handler_name = 'normal'
|
||||
else:
|
||||
handler_name = 'async'
|
||||
|
||||
handler = action_loader.get(
|
||||
handler_name,
|
||||
task=self._task,
|
||||
connection=connection,
|
||||
connection_info=self._connection_info,
|
||||
loader=self._loader,
|
||||
templar=templar,
|
||||
shared_loader_obj=self._shared_loader_obj,
|
||||
)
|
||||
|
||||
if not handler:
|
||||
raise AnsibleError("the handler '%s' was not found" % handler_name)
|
||||
|
||||
return handler
|
||||
|
||||
def _compute_delegate(self, variables):
|
||||
|
||||
# get the vars for the delegate by its name
|
||||
try:
|
||||
this_info = variables['hostvars'][self._task.delegate_to]
|
||||
except:
|
||||
# make sure the inject is empty for non-inventory hosts
|
||||
this_info = {}
|
||||
|
||||
# get the real ssh_address for the delegate and allow ansible_ssh_host to be templated
|
||||
#self._connection_info.remote_user = self._compute_delegate_user(self.delegate_to, delegate['inject'])
|
||||
self._connection_info.remote_addr = this_info.get('ansible_ssh_host', self._task.delegate_to)
|
||||
self._connection_info.port = this_info.get('ansible_ssh_port', self._connection_info.port)
|
||||
self._connection_info.password = this_info.get('ansible_ssh_pass', self._connection_info.password)
|
||||
self._connection_info.private_key_file = this_info.get('ansible_ssh_private_key_file', self._connection_info.private_key_file)
|
||||
self._connection_info.connection = this_info.get('ansible_connection', self._connection_info.connection)
|
||||
self._connection_info.become_pass = this_info.get('ansible_sudo_pass', self._connection_info.become_pass)
|
||||
|
||||
if self._connection_info.remote_addr in ('127.0.0.1', 'localhost'):
|
||||
self._connection_info.connection = 'local'
|
||||
|
||||
# Last chance to get private_key_file from global variables.
|
||||
# this is useful if delegated host is not defined in the inventory
|
||||
#if delegate['private_key_file'] is None:
|
||||
# delegate['private_key_file'] = remote_inject.get('ansible_ssh_private_key_file', None)
|
||||
|
||||
#if delegate['private_key_file'] is not None:
|
||||
# delegate['private_key_file'] = os.path.expanduser(delegate['private_key_file'])
|
||||
|
||||
for i in this_info:
|
||||
if i.startswith("ansible_") and i.endswith("_interpreter"):
|
||||
variables[i] = this_info[i]
|
||||
|
||||
233
lib/ansible/executor/task_queue_manager.py
Normal file
233
lib/ansible/executor/task_queue_manager.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# (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 multiprocessing
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.executor.connection_info import ConnectionInformation
|
||||
from ansible.executor.play_iterator import PlayIterator
|
||||
from ansible.executor.process.worker import WorkerProcess
|
||||
from ansible.executor.process.result import ResultProcess
|
||||
from ansible.executor.stats import AggregateStats
|
||||
from ansible.plugins import callback_loader, strategy_loader
|
||||
from ansible.template import Templar
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
__all__ = ['TaskQueueManager']
|
||||
|
||||
class TaskQueueManager:
|
||||
|
||||
'''
|
||||
This class handles the multiprocessing requirements of Ansible by
|
||||
creating a pool of worker forks, a result handler fork, and a
|
||||
manager object with shared datastructures/queues for coordinating
|
||||
work between all processes.
|
||||
|
||||
The queue manager is responsible for loading the play strategy plugin,
|
||||
which dispatches the Play's tasks to hosts.
|
||||
'''
|
||||
|
||||
def __init__(self, inventory, variable_manager, loader, display, options, passwords, stdout_callback=None):
|
||||
|
||||
self._inventory = inventory
|
||||
self._variable_manager = variable_manager
|
||||
self._loader = loader
|
||||
self._display = display
|
||||
self._options = options
|
||||
self._stats = AggregateStats()
|
||||
self.passwords = passwords
|
||||
|
||||
# a special flag to help us exit cleanly
|
||||
self._terminated = False
|
||||
|
||||
# this dictionary is used to keep track of notified handlers
|
||||
self._notified_handlers = dict()
|
||||
|
||||
# dictionaries to keep track of failed/unreachable hosts
|
||||
self._failed_hosts = dict()
|
||||
self._unreachable_hosts = dict()
|
||||
|
||||
self._final_q = multiprocessing.Queue()
|
||||
|
||||
# load callback plugins
|
||||
self._callback_plugins = self._load_callbacks(stdout_callback)
|
||||
|
||||
# create the pool of worker threads, based on the number of forks specified
|
||||
try:
|
||||
fileno = sys.stdin.fileno()
|
||||
except ValueError:
|
||||
fileno = None
|
||||
|
||||
self._workers = []
|
||||
for i in range(self._options.forks):
|
||||
main_q = multiprocessing.Queue()
|
||||
rslt_q = multiprocessing.Queue()
|
||||
|
||||
prc = WorkerProcess(self, main_q, rslt_q, loader)
|
||||
prc.start()
|
||||
|
||||
self._workers.append((prc, main_q, rslt_q))
|
||||
|
||||
self._result_prc = ResultProcess(self._final_q, self._workers)
|
||||
self._result_prc.start()
|
||||
|
||||
def _initialize_notified_handlers(self, handlers):
|
||||
'''
|
||||
Clears and initializes the shared notified handlers dict with entries
|
||||
for each handler in the play, which is an empty array that will contain
|
||||
inventory hostnames for those hosts triggering the handler.
|
||||
'''
|
||||
|
||||
# Zero the dictionary first by removing any entries there.
|
||||
# Proxied dicts don't support iteritems, so we have to use keys()
|
||||
for key in self._notified_handlers.keys():
|
||||
del self._notified_handlers[key]
|
||||
|
||||
# FIXME: there is a block compile helper for this...
|
||||
handler_list = []
|
||||
for handler_block in handlers:
|
||||
for handler in handler_block.block:
|
||||
handler_list.append(handler)
|
||||
|
||||
# then initialize it with the handler names from the handler list
|
||||
for handler in handler_list:
|
||||
self._notified_handlers[handler.get_name()] = []
|
||||
|
||||
def _load_callbacks(self, stdout_callback):
|
||||
'''
|
||||
Loads all available callbacks, with the exception of those which
|
||||
utilize the CALLBACK_TYPE option. When CALLBACK_TYPE is set to 'stdout',
|
||||
only one such callback plugin will be loaded.
|
||||
'''
|
||||
|
||||
loaded_plugins = []
|
||||
|
||||
stdout_callback_loaded = False
|
||||
if stdout_callback is None:
|
||||
stdout_callback = C.DEFAULT_STDOUT_CALLBACK
|
||||
|
||||
if stdout_callback not in callback_loader:
|
||||
raise AnsibleError("Invalid callback for stdout specified: %s" % stdout_callback)
|
||||
|
||||
for callback_plugin in callback_loader.all(class_only=True):
|
||||
if hasattr(callback_plugin, 'CALLBACK_VERSION') and callback_plugin.CALLBACK_VERSION >= 2.0:
|
||||
# we only allow one callback of type 'stdout' to be loaded, so check
|
||||
# the name of the current plugin and type to see if we need to skip
|
||||
# loading this callback plugin
|
||||
callback_type = getattr(callback_plugin, 'CALLBACK_TYPE', None)
|
||||
(callback_name, _) = os.path.splitext(os.path.basename(callback_plugin._original_path))
|
||||
if callback_type == 'stdout':
|
||||
if callback_name != stdout_callback or stdout_callback_loaded:
|
||||
continue
|
||||
stdout_callback_loaded = True
|
||||
|
||||
loaded_plugins.append(callback_plugin(self._display))
|
||||
else:
|
||||
loaded_plugins.append(callback_plugin())
|
||||
|
||||
return loaded_plugins
|
||||
|
||||
def run(self, play):
|
||||
'''
|
||||
Iterates over the roles/tasks in a play, using the given (or default)
|
||||
strategy for queueing tasks. The default is the linear strategy, which
|
||||
operates like classic Ansible by keeping all hosts in lock-step with
|
||||
a given task (meaning no hosts move on to the next task until all hosts
|
||||
are done with the current task).
|
||||
'''
|
||||
|
||||
all_vars = self._variable_manager.get_vars(loader=self._loader, play=play)
|
||||
templar = Templar(loader=self._loader, variables=all_vars, fail_on_undefined=False)
|
||||
|
||||
new_play = play.copy()
|
||||
new_play.post_validate(templar)
|
||||
|
||||
connection_info = ConnectionInformation(new_play, self._options, self.passwords)
|
||||
for callback_plugin in self._callback_plugins:
|
||||
if hasattr(callback_plugin, 'set_connection_info'):
|
||||
callback_plugin.set_connection_info(connection_info)
|
||||
|
||||
self.send_callback('v2_playbook_on_play_start', new_play)
|
||||
|
||||
# initialize the shared dictionary containing the notified handlers
|
||||
self._initialize_notified_handlers(new_play.handlers)
|
||||
|
||||
# load the specified strategy (or the default linear one)
|
||||
strategy = strategy_loader.get(new_play.strategy, self)
|
||||
if strategy is None:
|
||||
raise AnsibleError("Invalid play strategy specified: %s" % new_play.strategy, obj=play._ds)
|
||||
|
||||
# build the iterator
|
||||
iterator = PlayIterator(inventory=self._inventory, play=new_play, connection_info=connection_info, all_vars=all_vars)
|
||||
|
||||
# and run the play using the strategy
|
||||
return strategy.run(iterator, connection_info)
|
||||
|
||||
def cleanup(self):
|
||||
debug("RUNNING CLEANUP")
|
||||
|
||||
self.terminate()
|
||||
|
||||
self._final_q.close()
|
||||
self._result_prc.terminate()
|
||||
|
||||
for (worker_prc, main_q, rslt_q) in self._workers:
|
||||
rslt_q.close()
|
||||
main_q.close()
|
||||
worker_prc.terminate()
|
||||
|
||||
def get_inventory(self):
|
||||
return self._inventory
|
||||
|
||||
def get_variable_manager(self):
|
||||
return self._variable_manager
|
||||
|
||||
def get_loader(self):
|
||||
return self._loader
|
||||
|
||||
def get_notified_handlers(self):
|
||||
return self._notified_handlers
|
||||
|
||||
def get_workers(self):
|
||||
return self._workers[:]
|
||||
|
||||
def terminate(self):
|
||||
self._terminated = True
|
||||
|
||||
def send_callback(self, method_name, *args, **kwargs):
|
||||
for callback_plugin in self._callback_plugins:
|
||||
# a plugin that set self.disabled to True will not be called
|
||||
# see osx_say.py example for such a plugin
|
||||
if getattr(callback_plugin, 'disabled', False):
|
||||
continue
|
||||
methods = [
|
||||
getattr(callback_plugin, method_name, None),
|
||||
getattr(callback_plugin, 'on_any', None)
|
||||
]
|
||||
for method in methods:
|
||||
if method is not None:
|
||||
method(*args, **kwargs)
|
||||
|
||||
61
lib/ansible/executor/task_result.py
Normal file
61
lib/ansible/executor/task_result.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
|
||||
|
||||
from ansible.parsing import DataLoader
|
||||
|
||||
class TaskResult:
|
||||
'''
|
||||
This class is responsible for interpretting the resulting data
|
||||
from an executed task, and provides helper methods for determining
|
||||
the result of a given task.
|
||||
'''
|
||||
|
||||
def __init__(self, host, task, return_data):
|
||||
self._host = host
|
||||
self._task = task
|
||||
if isinstance(return_data, dict):
|
||||
self._result = return_data.copy()
|
||||
else:
|
||||
self._result = DataLoader().load(return_data)
|
||||
|
||||
def is_changed(self):
|
||||
return self._check_key('changed')
|
||||
|
||||
def is_skipped(self):
|
||||
return self._check_key('skipped')
|
||||
|
||||
def is_failed(self):
|
||||
if 'failed_when_result' in self._result:
|
||||
return self._check_key('failed_when_result')
|
||||
else:
|
||||
return self._check_key('failed') or self._result.get('rc', 0) != 0
|
||||
|
||||
def is_unreachable(self):
|
||||
return self._check_key('unreachable')
|
||||
|
||||
def _check_key(self, key):
|
||||
if 'results' in self._result:
|
||||
flag = False
|
||||
for res in self._result.get('results', []):
|
||||
if isinstance(res, dict):
|
||||
flag |= res.get(key, False)
|
||||
else:
|
||||
return self._result.get(key, False)
|
||||
70
lib/ansible/galaxy/__init__.py
Normal file
70
lib/ansible/galaxy/__init__.py
Normal file
@@ -0,0 +1,70 @@
|
||||
########################################################################
|
||||
#
|
||||
# (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/>.
|
||||
#
|
||||
########################################################################
|
||||
''' This manages remote shared Ansible objects, mainly roles'''
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.utils.display import Display
|
||||
|
||||
# default_readme_template
|
||||
# default_meta_template
|
||||
|
||||
|
||||
class Galaxy(object):
|
||||
''' Keeps global galaxy info '''
|
||||
|
||||
def __init__(self, options, display=None):
|
||||
|
||||
if display is None:
|
||||
self.display = Display()
|
||||
else:
|
||||
self.display = display
|
||||
|
||||
self.options = options
|
||||
self.roles_path = getattr(self.options, 'roles_path', None)
|
||||
if self.roles_path:
|
||||
self.roles_path = os.path.expanduser(self.roles_path)
|
||||
|
||||
self.roles = {}
|
||||
|
||||
# load data path for resource usage
|
||||
this_dir, this_filename = os.path.split(__file__)
|
||||
self.DATA_PATH = os.path.join(this_dir, "data")
|
||||
|
||||
#TODO: move to getter for lazy loading
|
||||
self.default_readme = self._str_from_data_file('readme')
|
||||
self.default_meta = self._str_from_data_file('metadata_template.j2')
|
||||
|
||||
def add_role(self, role):
|
||||
self.roles[role.name] = role
|
||||
|
||||
def remove_role(self, role_name):
|
||||
del self.roles[role_name]
|
||||
|
||||
|
||||
def _str_from_data_file(self, filename):
|
||||
myfile = os.path.join(self.DATA_PATH, filename)
|
||||
try:
|
||||
return open(myfile).read()
|
||||
except Exception as e:
|
||||
raise AnsibleError("Could not open %s: %s" % (filename, str(e)))
|
||||
|
||||
141
lib/ansible/galaxy/api.py
Executable file
141
lib/ansible/galaxy/api.py
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# (C) 2013, James Cammarata <jcammarata@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/>.
|
||||
#
|
||||
########################################################################
|
||||
import json
|
||||
from urllib2 import urlopen, quote as urlquote
|
||||
from urlparse import urlparse
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
class GalaxyAPI(object):
|
||||
''' This class is meant to be used as a API client for an Ansible Galaxy server '''
|
||||
|
||||
SUPPORTED_VERSIONS = ['v1']
|
||||
|
||||
def __init__(self, galaxy, api_server):
|
||||
|
||||
self.galaxy = galaxy
|
||||
|
||||
try:
|
||||
urlparse(api_server, scheme='https')
|
||||
except:
|
||||
raise AnsibleError("Invalid server API url passed: %s" % api_server)
|
||||
|
||||
server_version = self.get_server_api_version('%s/api/' % (api_server))
|
||||
if not server_version:
|
||||
raise AnsibleError("Could not retrieve server API version: %s" % api_server)
|
||||
|
||||
if server_version in self.SUPPORTED_VERSIONS:
|
||||
self.baseurl = '%s/api/%s' % (api_server, server_version)
|
||||
self.version = server_version # for future use
|
||||
self.galaxy.display.vvvvv("Base API: %s" % self.baseurl)
|
||||
else:
|
||||
raise AnsibleError("Unsupported Galaxy server API version: %s" % server_version)
|
||||
|
||||
def get_server_api_version(self, api_server):
|
||||
"""
|
||||
Fetches the Galaxy API current version to ensure
|
||||
the API server is up and reachable.
|
||||
"""
|
||||
#TODO: fix galaxy server which returns current_version path (/api/v1) vs actual version (v1)
|
||||
# also should set baseurl using supported_versions which has path
|
||||
return 'v1'
|
||||
|
||||
try:
|
||||
data = json.load(urlopen(api_server))
|
||||
return data.get("current_version", 'v1')
|
||||
except Exception as e:
|
||||
# TODO: report error
|
||||
return None
|
||||
|
||||
def lookup_role_by_name(self, role_name, notify=True):
|
||||
"""
|
||||
Find a role by name
|
||||
"""
|
||||
role_name = urlquote(role_name)
|
||||
|
||||
try:
|
||||
parts = role_name.split(".")
|
||||
user_name = ".".join(parts[0:-1])
|
||||
role_name = parts[-1]
|
||||
if notify:
|
||||
self.galaxy.display.display("- downloading role '%s', owned by %s" % (role_name, user_name))
|
||||
except:
|
||||
raise AnsibleError("- invalid role name (%s). Specify role as format: username.rolename" % role_name)
|
||||
|
||||
url = '%s/roles/?owner__username=%s&name=%s' % (self.baseurl, user_name, role_name)
|
||||
self.galaxy.display.vvvv("- %s" % (url))
|
||||
try:
|
||||
data = json.load(urlopen(url))
|
||||
if len(data["results"]) != 0:
|
||||
return data["results"][0]
|
||||
except:
|
||||
# TODO: report on connection/availability errors
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def fetch_role_related(self, related, role_id):
|
||||
"""
|
||||
Fetch the list of related items for the given role.
|
||||
The url comes from the 'related' field of the role.
|
||||
"""
|
||||
|
||||
try:
|
||||
url = '%s/roles/%d/%s/?page_size=50' % (self.baseurl, int(role_id), related)
|
||||
data = json.load(urlopen(url))
|
||||
results = data['results']
|
||||
done = (data.get('next', None) == None)
|
||||
while not done:
|
||||
url = '%s%s' % (self.baseurl, data['next'])
|
||||
self.galaxy.display.display(url)
|
||||
data = json.load(urlopen(url))
|
||||
results += data['results']
|
||||
done = (data.get('next', None) == None)
|
||||
return results
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_list(self, what):
|
||||
"""
|
||||
Fetch the list of items specified.
|
||||
"""
|
||||
|
||||
try:
|
||||
url = '%s/%s/?page_size' % (self.baseurl, what)
|
||||
data = json.load(urlopen(url))
|
||||
if "results" in data:
|
||||
results = data['results']
|
||||
else:
|
||||
results = data
|
||||
done = True
|
||||
if "next" in data:
|
||||
done = (data.get('next', None) == None)
|
||||
while not done:
|
||||
url = '%s%s' % (self.baseurl, data['next'])
|
||||
self.galaxy.display.display(url)
|
||||
data = json.load(urlopen(url))
|
||||
results += data['results']
|
||||
done = (data.get('next', None) == None)
|
||||
return results
|
||||
except Exception as error:
|
||||
raise AnsibleError("Failed to download the %s list: %s" % (what, str(error)))
|
||||
45
lib/ansible/galaxy/data/metadata_template.j2
Normal file
45
lib/ansible/galaxy/data/metadata_template.j2
Normal file
@@ -0,0 +1,45 @@
|
||||
galaxy_info:
|
||||
author: {{ author }}
|
||||
description: {{description}}
|
||||
company: {{ company }}
|
||||
# If the issue tracker for your role is not on github, uncomment the
|
||||
# next line and provide a value
|
||||
# issue_tracker_url: {{ issue_tracker_url }}
|
||||
# Some suggested licenses:
|
||||
# - BSD (default)
|
||||
# - MIT
|
||||
# - GPLv2
|
||||
# - GPLv3
|
||||
# - Apache
|
||||
# - CC-BY
|
||||
license: {{ license }}
|
||||
min_ansible_version: {{ min_ansible_version }}
|
||||
#
|
||||
# Below are all platforms currently available. Just uncomment
|
||||
# the ones that apply to your role. If you don't see your
|
||||
# platform on this list, let us know and we'll get it added!
|
||||
#
|
||||
#platforms:
|
||||
{%- for platform,versions in platforms.iteritems() %}
|
||||
#- name: {{ platform }}
|
||||
# versions:
|
||||
# - all
|
||||
{%- for version in versions %}
|
||||
# - {{ version }}
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
#
|
||||
# Below are all categories currently available. Just as with
|
||||
# the platforms above, uncomment those that apply to your role.
|
||||
#
|
||||
#categories:
|
||||
{%- for category in categories %}
|
||||
#- {{ category.name }}
|
||||
{%- endfor %}
|
||||
dependencies: []
|
||||
# List your role dependencies here, one per line.
|
||||
# Be sure to remove the '[]' above if you add dependencies
|
||||
# to this list.
|
||||
{% for dependency in dependencies %}
|
||||
#- {{ dependency }}
|
||||
{% endfor %}
|
||||
38
lib/ansible/galaxy/data/readme
Normal file
38
lib/ansible/galaxy/data/readme
Normal file
@@ -0,0 +1,38 @@
|
||||
Role Name
|
||||
=========
|
||||
|
||||
A brief description of the role goes here.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||
|
||||
Example Playbook
|
||||
----------------
|
||||
|
||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||
|
||||
- hosts: servers
|
||||
roles:
|
||||
- { role: username.rolename, x: 42 }
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
BSD
|
||||
|
||||
Author Information
|
||||
------------------
|
||||
|
||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
||||
295
lib/ansible/galaxy/role.py
Normal file
295
lib/ansible/galaxy/role.py
Normal file
@@ -0,0 +1,295 @@
|
||||
########################################################################
|
||||
#
|
||||
# (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/>.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import yaml
|
||||
from shutil import rmtree
|
||||
from urllib2 import urlopen
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
class GalaxyRole(object):
|
||||
|
||||
SUPPORTED_SCMS = set(['git', 'hg'])
|
||||
META_MAIN = os.path.join('meta', 'main.yml')
|
||||
META_INSTALL = os.path.join('meta', '.galaxy_install_info')
|
||||
ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars')
|
||||
|
||||
|
||||
def __init__(self, galaxy, name, src=None, version=None, scm=None):
|
||||
|
||||
self._metadata = None
|
||||
self._install_info = None
|
||||
|
||||
self.options = galaxy.options
|
||||
self.display = galaxy.display
|
||||
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.src = src
|
||||
self.scm = scm
|
||||
|
||||
self.path = (os.path.join(galaxy.roles_path, self.name))
|
||||
|
||||
def fetch_from_scm_archive(self):
|
||||
|
||||
# this can be configured to prevent unwanted SCMS but cannot add new ones unless the code is also updated
|
||||
if scm not in self.scms:
|
||||
self.display.display("The %s scm is not currently supported" % scm)
|
||||
return False
|
||||
|
||||
tempdir = tempfile.mkdtemp()
|
||||
clone_cmd = [scm, 'clone', role_url, self.name]
|
||||
with open('/dev/null', 'w') as devnull:
|
||||
try:
|
||||
self.display.display("- executing: %s" % " ".join(clone_cmd))
|
||||
popen = subprocess.Popen(clone_cmd, cwd=tempdir, stdout=devnull, stderr=devnull)
|
||||
except:
|
||||
raise AnsibleError("error executing: %s" % " ".join(clone_cmd))
|
||||
rc = popen.wait()
|
||||
if rc != 0:
|
||||
self.display.display("- command %s failed" % ' '.join(clone_cmd))
|
||||
self.display.display(" in directory %s" % tempdir)
|
||||
return False
|
||||
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.tar')
|
||||
if scm == 'hg':
|
||||
archive_cmd = ['hg', 'archive', '--prefix', "%s/" % self.name]
|
||||
if role_version:
|
||||
archive_cmd.extend(['-r', role_version])
|
||||
archive_cmd.append(temp_file.name)
|
||||
if scm == 'git':
|
||||
archive_cmd = ['git', 'archive', '--prefix=%s/' % self.name, '--output=%s' % temp_file.name]
|
||||
if role_version:
|
||||
archive_cmd.append(role_version)
|
||||
else:
|
||||
archive_cmd.append('HEAD')
|
||||
|
||||
with open('/dev/null', 'w') as devnull:
|
||||
self.display.display("- executing: %s" % " ".join(archive_cmd))
|
||||
popen = subprocess.Popen(archive_cmd, cwd=os.path.join(tempdir, self.name),
|
||||
stderr=devnull, stdout=devnull)
|
||||
rc = popen.wait()
|
||||
if rc != 0:
|
||||
self.display.display("- command %s failed" % ' '.join(archive_cmd))
|
||||
self.display.display(" in directory %s" % tempdir)
|
||||
return False
|
||||
|
||||
rmtree(tempdir, ignore_errors=True)
|
||||
|
||||
return temp_file.name
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
"""
|
||||
Returns role metadata
|
||||
"""
|
||||
if self._metadata is None:
|
||||
meta_path = os.path.join(self.path, self.META_MAIN)
|
||||
if os.path.isfile(meta_path):
|
||||
try:
|
||||
f = open(meta_path, 'r')
|
||||
self._metadata = yaml.safe_load(f)
|
||||
except:
|
||||
self.display.vvvvv("Unable to load metadata for %s" % self.name)
|
||||
return False
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
return self._metadata
|
||||
|
||||
|
||||
@property
|
||||
def install_info(self):
|
||||
"""
|
||||
Returns role install info
|
||||
"""
|
||||
if self._install_info is None:
|
||||
|
||||
info_path = os.path.join(self.path, self.META_INSTALL)
|
||||
if os.path.isfile(info_path):
|
||||
try:
|
||||
f = open(info_path, 'r')
|
||||
self._install_info = yaml.safe_load(f)
|
||||
except:
|
||||
self.display.vvvvv("Unable to load Galaxy install info for %s" % self.name)
|
||||
return False
|
||||
finally:
|
||||
f.close()
|
||||
return self._install_info
|
||||
|
||||
def _write_galaxy_install_info(self):
|
||||
"""
|
||||
Writes a YAML-formatted file to the role's meta/ directory
|
||||
(named .galaxy_install_info) which contains some information
|
||||
we can use later for commands like 'list' and 'info'.
|
||||
"""
|
||||
|
||||
info = dict(
|
||||
version=self.version,
|
||||
install_date=datetime.datetime.utcnow().strftime("%c"),
|
||||
)
|
||||
info_path = os.path.join(self.path, self.META_INSTALL)
|
||||
try:
|
||||
f = open(info_path, 'w+')
|
||||
self._install_info = yaml.safe_dump(info, f)
|
||||
except:
|
||||
return False
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
return True
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Removes the specified role from the roles path. There is a
|
||||
sanity check to make sure there's a meta/main.yml file at this
|
||||
path so the user doesn't blow away random directories
|
||||
"""
|
||||
if self.metadata:
|
||||
try:
|
||||
rmtree(self.path)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def fetch(self, target, role_data):
|
||||
"""
|
||||
Downloads the archived role from github to a temp location, extracts
|
||||
it, and then copies the extracted role to the role library path.
|
||||
"""
|
||||
|
||||
# first grab the file and save it to a temp location
|
||||
if self.src:
|
||||
archive_url = self.src
|
||||
else:
|
||||
archive_url = 'https://github.com/%s/%s/archive/%s.tar.gz' % (role_data["github_user"], role_data["github_repo"], target)
|
||||
self.display.display("- downloading role from %s" % archive_url)
|
||||
|
||||
try:
|
||||
url_file = urlopen(archive_url)
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
data = url_file.read()
|
||||
while data:
|
||||
temp_file.write(data)
|
||||
data = url_file.read()
|
||||
temp_file.close()
|
||||
return temp_file.name
|
||||
except:
|
||||
# TODO: better urllib2 error handling for error
|
||||
# messages that are more exact
|
||||
self.display.error("failed to download the file.")
|
||||
return False
|
||||
|
||||
def install(self, role_filename):
|
||||
# the file is a tar, so open it that way and extract it
|
||||
# to the specified (or default) roles directory
|
||||
|
||||
if not tarfile.is_tarfile(role_filename):
|
||||
self.display.error("the file downloaded was not a tar.gz")
|
||||
return False
|
||||
else:
|
||||
if role_filename.endswith('.gz'):
|
||||
role_tar_file = tarfile.open(role_filename, "r:gz")
|
||||
else:
|
||||
role_tar_file = tarfile.open(role_filename, "r")
|
||||
# verify the role's meta file
|
||||
meta_file = None
|
||||
members = role_tar_file.getmembers()
|
||||
# next find the metadata file
|
||||
for member in members:
|
||||
if self.META_MAIN in member.name:
|
||||
meta_file = member
|
||||
break
|
||||
if not meta_file:
|
||||
self.display.error("this role does not appear to have a meta/main.yml file.")
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
self._metadata = yaml.safe_load(role_tar_file.extractfile(meta_file))
|
||||
except:
|
||||
self.display.error("this role does not appear to have a valid meta/main.yml file.")
|
||||
return False
|
||||
|
||||
# we strip off the top-level directory for all of the files contained within
|
||||
# the tar file here, since the default is 'github_repo-target', and change it
|
||||
# to the specified role's name
|
||||
self.display.display("- extracting %s to %s" % (self.name, self.path))
|
||||
try:
|
||||
if os.path.exists(self.path):
|
||||
if not os.path.isdir(self.path):
|
||||
self.display.error("the specified roles path exists and is not a directory.")
|
||||
return False
|
||||
elif not getattr(self.options, "force", False):
|
||||
self.display.error("the specified role %s appears to already exist. Use --force to replace it." % self.name)
|
||||
return False
|
||||
else:
|
||||
# using --force, remove the old path
|
||||
if not self.remove():
|
||||
self.display.error("%s doesn't appear to contain a role." % self.path)
|
||||
self.display.error(" please remove this directory manually if you really want to put the role here.")
|
||||
return False
|
||||
else:
|
||||
os.makedirs(self.path)
|
||||
|
||||
# now we do the actual extraction to the path
|
||||
for member in members:
|
||||
# we only extract files, and remove any relative path
|
||||
# bits that might be in the file for security purposes
|
||||
# and drop the leading directory, as mentioned above
|
||||
if member.isreg() or member.issym():
|
||||
parts = member.name.split(os.sep)[1:]
|
||||
final_parts = []
|
||||
for part in parts:
|
||||
if part != '..' and '~' not in part and '$' not in part:
|
||||
final_parts.append(part)
|
||||
member.name = os.path.join(*final_parts)
|
||||
role_tar_file.extract(member, self.path)
|
||||
|
||||
# write out the install info file for later use
|
||||
self._write_galaxy_install_info()
|
||||
except OSError as e:
|
||||
self.display.error("Could not update files in %s: %s" % (self.path, str(e)))
|
||||
return False
|
||||
|
||||
# return the parsed yaml metadata
|
||||
self.display.display("- %s was installed successfully" % self.name)
|
||||
return True
|
||||
|
||||
@property
|
||||
def spec(self):
|
||||
"""
|
||||
Returns role spec info
|
||||
{
|
||||
'scm': 'git',
|
||||
'src': 'http://git.example.com/repos/repo.git',
|
||||
'version': 'v1.0',
|
||||
'name': 'repo'
|
||||
}
|
||||
"""
|
||||
return dict(scm=self.scm, src=self.src, version=self.version, name=self.name)
|
||||
@@ -16,20 +16,27 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible import constants as C
|
||||
from ansible.errors import *
|
||||
|
||||
from ansible.inventory.ini import InventoryParser
|
||||
from ansible.inventory.script import InventoryScript
|
||||
from ansible.inventory.dir import InventoryDirectory
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.inventory.host import Host
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
from ansible.plugins import vars_loader
|
||||
from ansible.utils.path import is_executable
|
||||
from ansible.utils.vars import combine_vars
|
||||
|
||||
class Inventory(object):
|
||||
"""
|
||||
@@ -40,12 +47,13 @@ class Inventory(object):
|
||||
'parser', '_vars_per_host', '_vars_per_group', '_hosts_cache', '_groups_list',
|
||||
'_pattern_cache', '_vault_password', '_vars_plugins', '_playbook_basedir']
|
||||
|
||||
def __init__(self, host_list=C.DEFAULT_HOST_LIST, vault_password=None):
|
||||
def __init__(self, loader, variable_manager, host_list=C.DEFAULT_HOST_LIST):
|
||||
|
||||
# the host file file, or script path, or list of hosts
|
||||
# if a list, inventory data will NOT be loaded
|
||||
self.host_list = host_list
|
||||
self._vault_password=vault_password
|
||||
self._loader = loader
|
||||
self._variable_manager = variable_manager
|
||||
|
||||
# caching to avoid repeated calculations, particularly with
|
||||
# external inventory scripts.
|
||||
@@ -97,7 +105,7 @@ class Inventory(object):
|
||||
if os.path.isdir(host_list):
|
||||
# Ensure basedir is inside the directory
|
||||
self.host_list = os.path.join(self.host_list, "")
|
||||
self.parser = InventoryDirectory(filename=host_list)
|
||||
self.parser = InventoryDirectory(loader=self._loader, filename=host_list)
|
||||
self.groups = self.parser.groups.values()
|
||||
else:
|
||||
# check to see if the specified file starts with a
|
||||
@@ -113,9 +121,9 @@ class Inventory(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if utils.is_executable(host_list):
|
||||
if is_executable(host_list):
|
||||
try:
|
||||
self.parser = InventoryScript(filename=host_list)
|
||||
self.parser = InventoryScript(loader=self._loader, filename=host_list)
|
||||
self.groups = self.parser.groups.values()
|
||||
except:
|
||||
if not shebang_present:
|
||||
@@ -134,19 +142,23 @@ class Inventory(object):
|
||||
else:
|
||||
raise
|
||||
|
||||
utils.plugins.vars_loader.add_directory(self.basedir(), with_subdir=True)
|
||||
vars_loader.add_directory(self.basedir(), with_subdir=True)
|
||||
else:
|
||||
raise errors.AnsibleError("Unable to find an inventory file, specify one with -i ?")
|
||||
|
||||
self._vars_plugins = [ x for x in utils.plugins.vars_loader.all(self) ]
|
||||
self._vars_plugins = [ x for x in vars_loader.all(self) ]
|
||||
|
||||
# FIXME: shouldn't be required, since the group/host vars file
|
||||
# management will be done in VariableManager
|
||||
# get group vars from group_vars/ files and vars plugins
|
||||
for group in self.groups:
|
||||
group.vars = utils.combine_vars(group.vars, self.get_group_variables(group.name, vault_password=self._vault_password))
|
||||
# FIXME: combine_vars
|
||||
group.vars = combine_vars(group.vars, self.get_group_variables(group.name))
|
||||
|
||||
# get host vars from host_vars/ files and vars plugins
|
||||
for host in self.get_hosts():
|
||||
host.vars = utils.combine_vars(host.vars, self.get_host_variables(host.name, vault_password=self._vault_password))
|
||||
# FIXME: combine_vars
|
||||
host.vars = combine_vars(host.vars, self.get_host_variables(host.name))
|
||||
|
||||
|
||||
def _match(self, str, pattern_str):
|
||||
@@ -192,9 +204,9 @@ class Inventory(object):
|
||||
|
||||
# exclude hosts mentioned in any restriction (ex: failed hosts)
|
||||
if self._restriction is not None:
|
||||
hosts = [ h for h in hosts if h.name in self._restriction ]
|
||||
hosts = [ h for h in hosts if h in self._restriction ]
|
||||
if self._also_restriction is not None:
|
||||
hosts = [ h for h in hosts if h.name in self._also_restriction ]
|
||||
hosts = [ h for h in hosts if h in self._also_restriction ]
|
||||
|
||||
return hosts
|
||||
|
||||
@@ -320,6 +332,8 @@ class Inventory(object):
|
||||
new_host = Host(pattern)
|
||||
new_host.set_variable("ansible_python_interpreter", sys.executable)
|
||||
new_host.set_variable("ansible_connection", "local")
|
||||
new_host.ipv4_address = '127.0.0.1'
|
||||
|
||||
ungrouped = self.get_group("ungrouped")
|
||||
if ungrouped is None:
|
||||
self.add_group(Group('ungrouped'))
|
||||
@@ -420,7 +434,7 @@ class Inventory(object):
|
||||
|
||||
group = self.get_group(groupname)
|
||||
if group is None:
|
||||
raise errors.AnsibleError("group not found: %s" % groupname)
|
||||
raise Exception("group not found: %s" % groupname)
|
||||
|
||||
vars = {}
|
||||
|
||||
@@ -428,19 +442,21 @@ class Inventory(object):
|
||||
vars_results = [ plugin.get_group_vars(group, vault_password=vault_password) for plugin in self._vars_plugins if hasattr(plugin, 'get_group_vars')]
|
||||
for updated in vars_results:
|
||||
if updated is not None:
|
||||
vars = utils.combine_vars(vars, updated)
|
||||
# FIXME: combine_vars
|
||||
vars = combine_vars(vars, updated)
|
||||
|
||||
# Read group_vars/ files
|
||||
vars = utils.combine_vars(vars, self.get_group_vars(group))
|
||||
# FIXME: combine_vars
|
||||
vars = combine_vars(vars, self.get_group_vars(group))
|
||||
|
||||
return vars
|
||||
|
||||
def get_variables(self, hostname, update_cached=False, vault_password=None):
|
||||
def get_vars(self, hostname, update_cached=False, vault_password=None):
|
||||
|
||||
host = self.get_host(hostname)
|
||||
if not host:
|
||||
raise errors.AnsibleError("host not found: %s" % hostname)
|
||||
return host.get_variables()
|
||||
raise Exception("host not found: %s" % hostname)
|
||||
return host.get_vars()
|
||||
|
||||
def get_host_variables(self, hostname, update_cached=False, vault_password=None):
|
||||
|
||||
@@ -460,22 +476,26 @@ class Inventory(object):
|
||||
vars_results = [ plugin.run(host, vault_password=vault_password) for plugin in self._vars_plugins if hasattr(plugin, 'run')]
|
||||
for updated in vars_results:
|
||||
if updated is not None:
|
||||
vars = utils.combine_vars(vars, updated)
|
||||
# FIXME: combine_vars
|
||||
vars = combine_vars(vars, updated)
|
||||
|
||||
# plugin.get_host_vars retrieves just vars for specific host
|
||||
vars_results = [ plugin.get_host_vars(host, vault_password=vault_password) for plugin in self._vars_plugins if hasattr(plugin, 'get_host_vars')]
|
||||
for updated in vars_results:
|
||||
if updated is not None:
|
||||
vars = utils.combine_vars(vars, updated)
|
||||
# FIXME: combine_vars
|
||||
vars = combine_vars(vars, updated)
|
||||
|
||||
# still need to check InventoryParser per host vars
|
||||
# which actually means InventoryScript per host,
|
||||
# which is not performant
|
||||
if self.parser is not None:
|
||||
vars = utils.combine_vars(vars, self.parser.get_host_variables(host))
|
||||
# FIXME: combine_vars
|
||||
vars = combine_vars(vars, self.parser.get_host_variables(host))
|
||||
|
||||
# Read host_vars/ files
|
||||
vars = utils.combine_vars(vars, self.get_host_vars(host))
|
||||
# FIXME: combine_vars
|
||||
vars = combine_vars(vars, self.get_host_vars(host))
|
||||
|
||||
return vars
|
||||
|
||||
@@ -490,7 +510,7 @@ class Inventory(object):
|
||||
|
||||
""" return a list of hostnames for a pattern """
|
||||
|
||||
result = [ h.name for h in self.get_hosts(pattern) ]
|
||||
result = [ h for h in self.get_hosts(pattern) ]
|
||||
if len(result) == 0 and pattern in ["localhost", "127.0.0.1"]:
|
||||
result = [pattern]
|
||||
return result
|
||||
@@ -498,11 +518,7 @@ class Inventory(object):
|
||||
def list_groups(self):
|
||||
return sorted([ g.name for g in self.groups ], key=lambda x: x)
|
||||
|
||||
# TODO: remove this function
|
||||
def get_restriction(self):
|
||||
return self._restriction
|
||||
|
||||
def restrict_to(self, restriction):
|
||||
def restrict_to_hosts(self, restriction):
|
||||
"""
|
||||
Restrict list operations to the hosts given in restriction. This is used
|
||||
to exclude failed hosts in main playbook code, don't use this for other
|
||||
@@ -544,7 +560,7 @@ class Inventory(object):
|
||||
results.append(x)
|
||||
self._subset = results
|
||||
|
||||
def lift_restriction(self):
|
||||
def remove_restriction(self):
|
||||
""" Do not restrict list operations """
|
||||
self._restriction = None
|
||||
|
||||
@@ -588,10 +604,12 @@ class Inventory(object):
|
||||
self._playbook_basedir = dir
|
||||
# get group vars from group_vars/ files
|
||||
for group in self.groups:
|
||||
group.vars = utils.combine_vars(group.vars, self.get_group_vars(group, new_pb_basedir=True))
|
||||
# FIXME: combine_vars
|
||||
group.vars = combine_vars(group.vars, self.get_group_vars(group, new_pb_basedir=True))
|
||||
# get host vars from host_vars/ files
|
||||
for host in self.get_hosts():
|
||||
host.vars = utils.combine_vars(host.vars, self.get_host_vars(host, new_pb_basedir=True))
|
||||
# FIXME: combine_vars
|
||||
host.vars = combine_vars(host.vars, self.get_host_vars(host, new_pb_basedir=True))
|
||||
# invalidate cache
|
||||
self._vars_per_host = {}
|
||||
self._vars_per_group = {}
|
||||
@@ -639,15 +657,15 @@ class Inventory(object):
|
||||
if _basedir == self._playbook_basedir and scan_pass != 1:
|
||||
continue
|
||||
|
||||
# FIXME: these should go to VariableManager
|
||||
if group and host is None:
|
||||
# load vars in dir/group_vars/name_of_group
|
||||
base_path = os.path.join(basedir, "group_vars/%s" % group.name)
|
||||
results = utils.load_vars(base_path, results, vault_password=self._vault_password)
|
||||
|
||||
self._variable_manager.add_group_vars_file(base_path, self._loader)
|
||||
elif host and group is None:
|
||||
# same for hostvars in dir/host_vars/name_of_host
|
||||
base_path = os.path.join(basedir, "host_vars/%s" % host.name)
|
||||
results = utils.load_vars(base_path, results, vault_password=self._vault_password)
|
||||
self._variable_manager.add_host_vars_file(base_path, self._loader)
|
||||
|
||||
# all done, results is a dictionary of variables for this particular host.
|
||||
return results
|
||||
|
||||
@@ -17,20 +17,25 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import ansible.constants as C
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.inventory.ini import InventoryParser
|
||||
from ansible.inventory.script import InventoryScript
|
||||
from ansible import utils
|
||||
from ansible import errors
|
||||
from ansible.utils.path import is_executable
|
||||
from ansible.utils.vars import combine_vars
|
||||
|
||||
class InventoryDirectory(object):
|
||||
''' Host inventory parser for ansible using a directory of inventories. '''
|
||||
|
||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
||||
def __init__(self, loader, filename=C.DEFAULT_HOST_LIST):
|
||||
self.names = os.listdir(filename)
|
||||
self.names.sort()
|
||||
self.directory = filename
|
||||
@@ -38,10 +43,12 @@ class InventoryDirectory(object):
|
||||
self.hosts = {}
|
||||
self.groups = {}
|
||||
|
||||
self._loader = loader
|
||||
|
||||
for i in self.names:
|
||||
|
||||
# Skip files that end with certain extensions or characters
|
||||
if any(i.endswith(ext) for ext in ("~", ".orig", ".bak", ".ini", ".retry", ".pyc", ".pyo")):
|
||||
if any(i.endswith(ext) for ext in ("~", ".orig", ".bak", ".ini", ".cfg", ".retry", ".pyc", ".pyo")):
|
||||
continue
|
||||
# Skip hidden files
|
||||
if i.startswith('.') and not i.startswith('./'):
|
||||
@@ -51,9 +58,9 @@ class InventoryDirectory(object):
|
||||
continue
|
||||
fullpath = os.path.join(self.directory, i)
|
||||
if os.path.isdir(fullpath):
|
||||
parser = InventoryDirectory(filename=fullpath)
|
||||
elif utils.is_executable(fullpath):
|
||||
parser = InventoryScript(filename=fullpath)
|
||||
parser = InventoryDirectory(loader=loader, filename=fullpath)
|
||||
elif is_executable(fullpath):
|
||||
parser = InventoryScript(loader=loader, filename=fullpath)
|
||||
else:
|
||||
parser = InventoryParser(filename=fullpath)
|
||||
self.parsers.append(parser)
|
||||
@@ -153,7 +160,7 @@ class InventoryDirectory(object):
|
||||
|
||||
# name
|
||||
if group.name != newgroup.name:
|
||||
raise errors.AnsibleError("Cannot merge group %s with %s" % (group.name, newgroup.name))
|
||||
raise AnsibleError("Cannot merge group %s with %s" % (group.name, newgroup.name))
|
||||
|
||||
# depth
|
||||
group.depth = max([group.depth, newgroup.depth])
|
||||
@@ -196,14 +203,14 @@ class InventoryDirectory(object):
|
||||
self.groups[newparent.name].add_child_group(group)
|
||||
|
||||
# variables
|
||||
group.vars = utils.combine_vars(group.vars, newgroup.vars)
|
||||
group.vars = combine_vars(group.vars, newgroup.vars)
|
||||
|
||||
def _merge_hosts(self,host, newhost):
|
||||
""" Merge all of instance newhost into host """
|
||||
|
||||
# name
|
||||
if host.name != newhost.name:
|
||||
raise errors.AnsibleError("Cannot merge host %s with %s" % (host.name, newhost.name))
|
||||
raise AnsibleError("Cannot merge host %s with %s" % (host.name, newhost.name))
|
||||
|
||||
# group membership relation
|
||||
for newgroup in newhost.groups:
|
||||
@@ -218,7 +225,7 @@ class InventoryDirectory(object):
|
||||
self.groups[newgroup.name].add_host(host)
|
||||
|
||||
# variables
|
||||
host.vars = utils.combine_vars(host.vars, newhost.vars)
|
||||
host.vars = combine_vars(host.vars, newhost.vars)
|
||||
|
||||
def get_host_variables(self, host):
|
||||
""" Gets additional host variables from all inventories """
|
||||
|
||||
@@ -30,6 +30,9 @@ expanded into 001, 002 ...009, 010.
|
||||
Note that when beg is specified with left zero padding, then the length of
|
||||
end must be the same as that of beg, else an exception is raised.
|
||||
'''
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import string
|
||||
|
||||
from ansible import errors
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
class Group(object):
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
class Group:
|
||||
''' a group of ansible hosts '''
|
||||
|
||||
__slots__ = [ 'name', 'hosts', 'vars', 'child_groups', 'parent_groups', 'depth', '_hosts_cache' ]
|
||||
#__slots__ = [ 'name', 'hosts', 'vars', 'child_groups', 'parent_groups', 'depth', '_hosts_cache' ]
|
||||
|
||||
def __init__(self, name=None):
|
||||
|
||||
@@ -29,9 +33,49 @@ class Group(object):
|
||||
self.child_groups = []
|
||||
self.parent_groups = []
|
||||
self._hosts_cache = None
|
||||
|
||||
#self.clear_hosts_cache()
|
||||
if self.name is None:
|
||||
raise Exception("group name is required")
|
||||
#if self.name is None:
|
||||
# raise Exception("group name is required")
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_name()
|
||||
|
||||
def __getstate__(self):
|
||||
return self.serialize()
|
||||
|
||||
def __setstate__(self, data):
|
||||
return self.deserialize(data)
|
||||
|
||||
def serialize(self):
|
||||
parent_groups = []
|
||||
for parent in self.parent_groups:
|
||||
parent_groups.append(parent.serialize())
|
||||
|
||||
result = dict(
|
||||
name=self.name,
|
||||
vars=self.vars.copy(),
|
||||
parent_groups=parent_groups,
|
||||
depth=self.depth,
|
||||
)
|
||||
|
||||
debug("serializing group, result is: %s" % result)
|
||||
return result
|
||||
|
||||
def deserialize(self, data):
|
||||
debug("deserializing group, data is: %s" % data)
|
||||
self.__init__()
|
||||
self.name = data.get('name')
|
||||
self.vars = data.get('vars', dict())
|
||||
|
||||
parent_groups = data.get('parent_groups', [])
|
||||
for parent_data in parent_groups:
|
||||
g = Group()
|
||||
g.deserialize(parent_data)
|
||||
self.parent_groups.append(g)
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def add_child_group(self, group):
|
||||
|
||||
@@ -100,7 +144,7 @@ class Group(object):
|
||||
hosts.append(mine)
|
||||
return hosts
|
||||
|
||||
def get_variables(self):
|
||||
def get_vars(self):
|
||||
return self.vars.copy()
|
||||
|
||||
def _get_ancestors(self):
|
||||
|
||||
@@ -15,24 +15,88 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible import utils
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
class Host(object):
|
||||
from ansible import constants as C
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.utils.vars import combine_vars
|
||||
|
||||
__all__ = ['Host']
|
||||
|
||||
class Host:
|
||||
''' a single ansible host '''
|
||||
|
||||
__slots__ = [ 'name', 'vars', 'groups' ]
|
||||
#__slots__ = [ 'name', 'vars', 'groups' ]
|
||||
|
||||
def __getstate__(self):
|
||||
return self.serialize()
|
||||
|
||||
def __setstate__(self, data):
|
||||
return self.deserialize(data)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name
|
||||
|
||||
def serialize(self):
|
||||
groups = []
|
||||
for group in self.groups:
|
||||
groups.append(group.serialize())
|
||||
|
||||
return dict(
|
||||
name=self.name,
|
||||
vars=self.vars.copy(),
|
||||
ipv4_address=self.ipv4_address,
|
||||
ipv6_address=self.ipv6_address,
|
||||
port=self.port,
|
||||
gathered_facts=self._gathered_facts,
|
||||
groups=groups,
|
||||
)
|
||||
|
||||
def deserialize(self, data):
|
||||
self.__init__()
|
||||
|
||||
self.name = data.get('name')
|
||||
self.vars = data.get('vars', dict())
|
||||
self.ipv4_address = data.get('ipv4_address', '')
|
||||
self.ipv6_address = data.get('ipv6_address', '')
|
||||
self.port = data.get('port')
|
||||
|
||||
groups = data.get('groups', [])
|
||||
for group_data in groups:
|
||||
g = Group()
|
||||
g.deserialize(group_data)
|
||||
self.groups.append(g)
|
||||
|
||||
def __init__(self, name=None, port=None):
|
||||
|
||||
self.name = name
|
||||
self.vars = {}
|
||||
self.groups = []
|
||||
if port and port != C.DEFAULT_REMOTE_PORT:
|
||||
self.set_variable('ansible_ssh_port', int(port))
|
||||
|
||||
if self.name is None:
|
||||
raise Exception("host name is required")
|
||||
self.ipv4_address = name
|
||||
self.ipv6_address = name
|
||||
|
||||
if port and port != C.DEFAULT_REMOTE_PORT:
|
||||
self.port = int(port)
|
||||
else:
|
||||
self.port = C.DEFAULT_REMOTE_PORT
|
||||
|
||||
self._gathered_facts = False
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_name()
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def gathered_facts(self):
|
||||
return self._gathered_facts
|
||||
|
||||
def set_gathered_facts(self, gathered):
|
||||
self._gathered_facts = gathered
|
||||
|
||||
def add_group(self, group):
|
||||
|
||||
@@ -52,16 +116,15 @@ class Host(object):
|
||||
groups[a.name] = a
|
||||
return groups.values()
|
||||
|
||||
def get_variables(self):
|
||||
def get_vars(self):
|
||||
|
||||
results = {}
|
||||
groups = self.get_groups()
|
||||
for group in sorted(groups, key=lambda g: g.depth):
|
||||
results = utils.combine_vars(results, group.get_variables())
|
||||
results = utils.combine_vars(results, self.vars)
|
||||
results = combine_vars(results, group.get_vars())
|
||||
results = combine_vars(results, self.vars)
|
||||
results['inventory_hostname'] = self.name
|
||||
results['inventory_hostname_short'] = self.name.split('.')[0]
|
||||
results['group_names'] = sorted([ g.name for g in groups if g.name != 'all'])
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -16,17 +16,20 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import ansible.constants as C
|
||||
import ast
|
||||
import shlex
|
||||
import re
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import *
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.inventory.expand_hosts import detect_range
|
||||
from ansible.inventory.expand_hosts import expand_hostname_range
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
import shlex
|
||||
import re
|
||||
import ast
|
||||
from ansible.utils.unicode import to_unicode
|
||||
|
||||
class InventoryParser(object):
|
||||
"""
|
||||
@@ -34,9 +37,8 @@ class InventoryParser(object):
|
||||
"""
|
||||
|
||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
||||
|
||||
self.filename = filename
|
||||
with open(filename) as fh:
|
||||
self.filename = filename
|
||||
self.lines = fh.readlines()
|
||||
self.groups = {}
|
||||
self.hosts = {}
|
||||
@@ -54,10 +56,7 @@ class InventoryParser(object):
|
||||
def _parse_value(v):
|
||||
if "#" not in v:
|
||||
try:
|
||||
ret = ast.literal_eval(v)
|
||||
if not isinstance(ret, float):
|
||||
# Do not trim floats. Eg: "1.20" to 1.2
|
||||
return ret
|
||||
v = ast.literal_eval(v)
|
||||
# Using explicit exceptions.
|
||||
# Likely a string that literal_eval does not like. We wil then just set it.
|
||||
except ValueError:
|
||||
@@ -66,7 +65,7 @@ class InventoryParser(object):
|
||||
except SyntaxError:
|
||||
# Is this a hash with an equals at the end?
|
||||
pass
|
||||
return v
|
||||
return to_unicode(v, nonstring='passthru', errors='strict')
|
||||
|
||||
# [webservers]
|
||||
# alpha
|
||||
@@ -91,8 +90,8 @@ class InventoryParser(object):
|
||||
self.groups = dict(all=all, ungrouped=ungrouped)
|
||||
active_group_name = 'ungrouped'
|
||||
|
||||
for lineno in range(len(self.lines)):
|
||||
line = utils.before_comment(self.lines[lineno]).strip()
|
||||
for line in self.lines:
|
||||
line = self._before_comment(line).strip()
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
active_group_name = line.replace("[","").replace("]","")
|
||||
if ":vars" in line or ":children" in line:
|
||||
@@ -146,8 +145,11 @@ class InventoryParser(object):
|
||||
try:
|
||||
(k,v) = t.split("=", 1)
|
||||
except ValueError, e:
|
||||
raise errors.AnsibleError("%s:%s: Invalid ini entry: %s - %s" % (self.filename, lineno + 1, t, str(e)))
|
||||
host.set_variable(k, self._parse_value(v))
|
||||
raise AnsibleError("Invalid ini entry in %s: %s - %s" % (self.filename, t, str(e)))
|
||||
if k == 'ansible_ssh_host':
|
||||
host.ipv4_address = self._parse_value(v)
|
||||
else:
|
||||
host.set_variable(k, self._parse_value(v))
|
||||
self.groups[active_group_name].add_host(host)
|
||||
|
||||
# [southeast:children]
|
||||
@@ -157,8 +159,8 @@ class InventoryParser(object):
|
||||
def _parse_group_children(self):
|
||||
group = None
|
||||
|
||||
for lineno in range(len(self.lines)):
|
||||
line = self.lines[lineno].strip()
|
||||
for line in self.lines:
|
||||
line = line.strip()
|
||||
if line is None or line == '':
|
||||
continue
|
||||
if line.startswith("[") and ":children]" in line:
|
||||
@@ -173,7 +175,7 @@ class InventoryParser(object):
|
||||
elif group:
|
||||
kid_group = self.groups.get(line, None)
|
||||
if kid_group is None:
|
||||
raise errors.AnsibleError("%s:%d: child group is not defined: (%s)" % (self.filename, lineno + 1, line))
|
||||
raise AnsibleError("child group is not defined: (%s)" % line)
|
||||
else:
|
||||
group.add_child_group(kid_group)
|
||||
|
||||
@@ -184,13 +186,13 @@ class InventoryParser(object):
|
||||
|
||||
def _parse_group_variables(self):
|
||||
group = None
|
||||
for lineno in range(len(self.lines)):
|
||||
line = self.lines[lineno].strip()
|
||||
for line in self.lines:
|
||||
line = line.strip()
|
||||
if line.startswith("[") and ":vars]" in line:
|
||||
line = line.replace("[","").replace(":vars]","")
|
||||
group = self.groups.get(line, None)
|
||||
if group is None:
|
||||
raise errors.AnsibleError("%s:%d: can't add vars to undefined group: %s" % (self.filename, lineno + 1, line))
|
||||
raise AnsibleError("can't add vars to undefined group: %s" % line)
|
||||
elif line.startswith("#") or line.startswith(";"):
|
||||
pass
|
||||
elif line.startswith("["):
|
||||
@@ -199,10 +201,18 @@ class InventoryParser(object):
|
||||
pass
|
||||
elif group:
|
||||
if "=" not in line:
|
||||
raise errors.AnsibleError("%s:%d: variables assigned to group must be in key=value form" % (self.filename, lineno + 1))
|
||||
raise AnsibleError("variables assigned to group must be in key=value form")
|
||||
else:
|
||||
(k, v) = [e.strip() for e in line.split("=", 1)]
|
||||
group.set_variable(k, self._parse_value(v))
|
||||
|
||||
def get_host_variables(self, host):
|
||||
return {}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -16,22 +16,26 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import ansible.constants as C
|
||||
import sys
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import *
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.module_utils.basic import json_dict_bytes_to_unicode
|
||||
from ansible import utils
|
||||
from ansible import errors
|
||||
import sys
|
||||
|
||||
|
||||
class InventoryScript(object):
|
||||
class InventoryScript:
|
||||
''' Host inventory parser for ansible using external inventory scripts. '''
|
||||
|
||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
||||
def __init__(self, loader, filename=C.DEFAULT_HOST_LIST):
|
||||
|
||||
self._loader = loader
|
||||
|
||||
# Support inventory scripts that are not prefixed with some
|
||||
# path information but happen to be in the current working
|
||||
@@ -41,11 +45,11 @@ class InventoryScript(object):
|
||||
try:
|
||||
sp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
except OSError, e:
|
||||
raise errors.AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
|
||||
raise AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
|
||||
(stdout, stderr) = sp.communicate()
|
||||
|
||||
if sp.returncode != 0:
|
||||
raise errors.AnsibleError("Inventory script (%s) had an execution error: %s " % (filename,stderr))
|
||||
raise AnsibleError("Inventory script (%s) had an execution error: %s " % (filename,stderr))
|
||||
|
||||
self.data = stdout
|
||||
# see comment about _meta below
|
||||
@@ -58,7 +62,7 @@ class InventoryScript(object):
|
||||
all_hosts = {}
|
||||
|
||||
# not passing from_remote because data from CMDB is trusted
|
||||
self.raw = utils.parse_json(self.data)
|
||||
self.raw = self._loader.load(self.data)
|
||||
self.raw = json_dict_bytes_to_unicode(self.raw)
|
||||
|
||||
all = Group('all')
|
||||
@@ -68,7 +72,7 @@ class InventoryScript(object):
|
||||
|
||||
if 'failed' in self.raw:
|
||||
sys.stderr.write(err + "\n")
|
||||
raise errors.AnsibleError("failed to parse executable inventory script results: %s" % self.raw)
|
||||
raise AnsibleError("failed to parse executable inventory script results: %s" % self.raw)
|
||||
|
||||
for (group_name, data) in self.raw.items():
|
||||
|
||||
@@ -92,12 +96,12 @@ class InventoryScript(object):
|
||||
if not isinstance(data, dict):
|
||||
data = {'hosts': data}
|
||||
# is not those subkeys, then simplified syntax, host with vars
|
||||
elif not any(k in data for k in ('hosts','vars','children')):
|
||||
elif not any(k in data for k in ('hosts','vars')):
|
||||
data = {'hosts': [group_name], 'vars': data}
|
||||
|
||||
if 'hosts' in data:
|
||||
if not isinstance(data['hosts'], list):
|
||||
raise errors.AnsibleError("You defined a group \"%s\" with bad "
|
||||
raise AnsibleError("You defined a group \"%s\" with bad "
|
||||
"data for the host list:\n %s" % (group_name, data))
|
||||
|
||||
for hostname in data['hosts']:
|
||||
@@ -108,7 +112,7 @@ class InventoryScript(object):
|
||||
|
||||
if 'vars' in data:
|
||||
if not isinstance(data['vars'], dict):
|
||||
raise errors.AnsibleError("You defined a group \"%s\" with bad "
|
||||
raise AnsibleError("You defined a group \"%s\" with bad "
|
||||
"data for variables:\n %s" % (group_name, data))
|
||||
|
||||
for k, v in data['vars'].iteritems():
|
||||
@@ -143,12 +147,12 @@ class InventoryScript(object):
|
||||
try:
|
||||
sp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
except OSError, e:
|
||||
raise errors.AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
|
||||
raise AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
|
||||
(out, err) = sp.communicate()
|
||||
if out.strip() == '':
|
||||
return dict()
|
||||
try:
|
||||
return json_dict_bytes_to_unicode(utils.parse_json(out))
|
||||
return json_dict_bytes_to_unicode(self._loader.load(out))
|
||||
except ValueError:
|
||||
raise errors.AnsibleError("could not parse post variable response: %s, %s" % (cmd, out))
|
||||
raise AnsibleError("could not parse post variable response: %s, %s" % (cmd, out))
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
class VarsModule(object):
|
||||
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
# (c) 2013-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 python and deps
|
||||
from cStringIO import StringIO
|
||||
import inspect
|
||||
import os
|
||||
import shlex
|
||||
|
||||
# from Ansible
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
from ansible import constants as C
|
||||
from ansible import __version__
|
||||
from ansible.utils.unicode import to_bytes
|
||||
|
||||
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
|
||||
REPLACER_ARGS = "\"<<INCLUDE_ANSIBLE_MODULE_ARGS>>\""
|
||||
REPLACER_COMPLEX = "\"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>\""
|
||||
REPLACER_WINDOWS = "# POWERSHELL_COMMON"
|
||||
REPLACER_VERSION = "\"<<ANSIBLE_VERSION>>\""
|
||||
REPLACER_SELINUX = "<<SELINUX_SPECIAL_FILESYSTEMS>>"
|
||||
|
||||
|
||||
class ModuleReplacer(object):
|
||||
|
||||
"""
|
||||
The Replacer is used to insert chunks of code into modules before
|
||||
transfer. Rather than doing classical python imports, this allows for more
|
||||
efficient transfer in a no-bootstrapping scenario by not moving extra files
|
||||
over the wire, and also takes care of embedding arguments in the transferred
|
||||
modules.
|
||||
|
||||
This version is done in such a way that local imports can still be
|
||||
used in the module code, so IDEs don't have to be aware of what is going on.
|
||||
|
||||
Example:
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
... will result in the insertion basic.py into the module
|
||||
|
||||
from the module_utils/ directory in the source tree.
|
||||
|
||||
All modules are required to import at least basic, though there will also
|
||||
be other snippets.
|
||||
|
||||
# POWERSHELL_COMMON
|
||||
|
||||
Also results in the inclusion of the common code in powershell.ps1
|
||||
|
||||
"""
|
||||
|
||||
# ******************************************************************************
|
||||
|
||||
def __init__(self, strip_comments=False):
|
||||
this_file = inspect.getfile(inspect.currentframe())
|
||||
self.snippet_path = os.path.join(os.path.dirname(this_file), 'module_utils')
|
||||
self.strip_comments = strip_comments # TODO: implement
|
||||
|
||||
# ******************************************************************************
|
||||
|
||||
|
||||
def slurp(self, path):
|
||||
if not os.path.exists(path):
|
||||
raise errors.AnsibleError("imported module support code does not exist at %s" % path)
|
||||
fd = open(path)
|
||||
data = fd.read()
|
||||
fd.close()
|
||||
return data
|
||||
|
||||
def _find_snippet_imports(self, module_data, module_path):
|
||||
"""
|
||||
Given the source of the module, convert it to a Jinja2 template to insert
|
||||
module code and return whether it's a new or old style module.
|
||||
"""
|
||||
|
||||
module_style = 'old'
|
||||
if REPLACER in module_data:
|
||||
module_style = 'new'
|
||||
elif 'from ansible.module_utils.' in module_data:
|
||||
module_style = 'new'
|
||||
elif 'WANT_JSON' in module_data:
|
||||
module_style = 'non_native_want_json'
|
||||
|
||||
output = StringIO()
|
||||
lines = module_data.split('\n')
|
||||
snippet_names = []
|
||||
|
||||
for line in lines:
|
||||
|
||||
if REPLACER in line:
|
||||
output.write(self.slurp(os.path.join(self.snippet_path, "basic.py")))
|
||||
snippet_names.append('basic')
|
||||
if REPLACER_WINDOWS in line:
|
||||
ps_data = self.slurp(os.path.join(self.snippet_path, "powershell.ps1"))
|
||||
output.write(ps_data)
|
||||
snippet_names.append('powershell')
|
||||
elif line.startswith('from ansible.module_utils.'):
|
||||
tokens=line.split(".")
|
||||
import_error = False
|
||||
if len(tokens) != 3:
|
||||
import_error = True
|
||||
if " import *" not in line:
|
||||
import_error = True
|
||||
if import_error:
|
||||
raise errors.AnsibleError("error importing module in %s, expecting format like 'from ansible.module_utils.basic import *'" % module_path)
|
||||
snippet_name = tokens[2].split()[0]
|
||||
snippet_names.append(snippet_name)
|
||||
output.write(self.slurp(os.path.join(self.snippet_path, snippet_name + ".py")))
|
||||
|
||||
else:
|
||||
if self.strip_comments and line.startswith("#") or line == '':
|
||||
pass
|
||||
output.write(line)
|
||||
output.write("\n")
|
||||
|
||||
if not module_path.endswith(".ps1"):
|
||||
# Unixy modules
|
||||
if len(snippet_names) > 0 and not 'basic' in snippet_names:
|
||||
raise errors.AnsibleError("missing required import in %s: from ansible.module_utils.basic import *" % module_path)
|
||||
else:
|
||||
# Windows modules
|
||||
if len(snippet_names) > 0 and not 'powershell' in snippet_names:
|
||||
raise errors.AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path)
|
||||
|
||||
return (output.getvalue(), module_style)
|
||||
|
||||
# ******************************************************************************
|
||||
|
||||
def modify_module(self, module_path, complex_args, module_args, inject):
|
||||
|
||||
with open(module_path) as f:
|
||||
|
||||
# read in the module source
|
||||
module_data = f.read()
|
||||
|
||||
(module_data, module_style) = self._find_snippet_imports(module_data, module_path)
|
||||
|
||||
complex_args_json = utils.jsonify(complex_args)
|
||||
# We force conversion of module_args to str because module_common calls shlex.split,
|
||||
# a standard library function that incorrectly handles Unicode input before Python 2.7.3.
|
||||
# Note: it would be better to do all this conversion at the border
|
||||
# (when the data is originally parsed into data structures) but
|
||||
# it's currently coming from too many sources to make that
|
||||
# effective.
|
||||
try:
|
||||
encoded_args = repr(module_args.encode('utf-8'))
|
||||
except UnicodeDecodeError:
|
||||
encoded_args = repr(module_args)
|
||||
try:
|
||||
encoded_complex = repr(complex_args_json.encode('utf-8'))
|
||||
except UnicodeDecodeError:
|
||||
encoded_complex = repr(complex_args_json.encode('utf-8'))
|
||||
|
||||
# these strings should be part of the 'basic' snippet which is required to be included
|
||||
module_data = module_data.replace(REPLACER_VERSION, repr(__version__))
|
||||
module_data = module_data.replace(REPLACER_SELINUX, ','.join(C.DEFAULT_SELINUX_SPECIAL_FS))
|
||||
module_data = module_data.replace(REPLACER_ARGS, encoded_args)
|
||||
module_data = module_data.replace(REPLACER_COMPLEX, encoded_complex)
|
||||
|
||||
if module_style == 'new':
|
||||
facility = C.DEFAULT_SYSLOG_FACILITY
|
||||
if 'ansible_syslog_facility' in inject:
|
||||
facility = inject['ansible_syslog_facility']
|
||||
module_data = module_data.replace('syslog.LOG_USER', "syslog.%s" % facility)
|
||||
|
||||
lines = module_data.split("\n")
|
||||
shebang = None
|
||||
if lines[0].startswith("#!"):
|
||||
shebang = lines[0].strip()
|
||||
args = shlex.split(str(shebang[2:]))
|
||||
interpreter = args[0]
|
||||
interpreter_config = 'ansible_%s_interpreter' % os.path.basename(interpreter)
|
||||
|
||||
if interpreter_config in inject:
|
||||
interpreter = to_bytes(inject[interpreter_config], errors='strict')
|
||||
lines[0] = shebang = "#!%s %s" % (interpreter, " ".join(args[1:]))
|
||||
module_data = "\n".join(lines)
|
||||
|
||||
return (module_data, module_style, shebang)
|
||||
|
||||
@@ -45,7 +45,7 @@ SELINUX_SPECIAL_FS="<<SELINUX_SPECIAL_FILESYSTEMS>>"
|
||||
# can be inserted in any module source automatically by including
|
||||
# #<<INCLUDE_ANSIBLE_MODULE_COMMON>> on a blank line by itself inside
|
||||
# of an ansible module. The source of this common code lives
|
||||
# in lib/ansible/module_common.py
|
||||
# in ansible/executor/module_common.py
|
||||
|
||||
import locale
|
||||
import os
|
||||
@@ -67,6 +67,7 @@ import pwd
|
||||
import platform
|
||||
import errno
|
||||
import tempfile
|
||||
from itertools import imap, repeat
|
||||
|
||||
try:
|
||||
import json
|
||||
@@ -237,7 +238,7 @@ def load_platform_subclass(cls, *args, **kwargs):
|
||||
return super(cls, subclass).__new__(subclass)
|
||||
|
||||
|
||||
def json_dict_unicode_to_bytes(d):
|
||||
def json_dict_unicode_to_bytes(d, encoding='utf-8'):
|
||||
''' Recursively convert dict keys and values to byte str
|
||||
|
||||
Specialized for json return because this only handles, lists, tuples,
|
||||
@@ -245,17 +246,17 @@ def json_dict_unicode_to_bytes(d):
|
||||
'''
|
||||
|
||||
if isinstance(d, unicode):
|
||||
return d.encode('utf-8')
|
||||
return d.encode(encoding)
|
||||
elif isinstance(d, dict):
|
||||
return dict(map(json_dict_unicode_to_bytes, d.iteritems()))
|
||||
return dict(imap(json_dict_unicode_to_bytes, d.iteritems(), repeat(encoding)))
|
||||
elif isinstance(d, list):
|
||||
return list(map(json_dict_unicode_to_bytes, d))
|
||||
return list(imap(json_dict_unicode_to_bytes, d, repeat(encoding)))
|
||||
elif isinstance(d, tuple):
|
||||
return tuple(map(json_dict_unicode_to_bytes, d))
|
||||
return tuple(imap(json_dict_unicode_to_bytes, d, repeat(encoding)))
|
||||
else:
|
||||
return d
|
||||
|
||||
def json_dict_bytes_to_unicode(d):
|
||||
def json_dict_bytes_to_unicode(d, encoding='utf-8'):
|
||||
''' Recursively convert dict keys and values to byte str
|
||||
|
||||
Specialized for json return because this only handles, lists, tuples,
|
||||
@@ -263,13 +264,13 @@ def json_dict_bytes_to_unicode(d):
|
||||
'''
|
||||
|
||||
if isinstance(d, str):
|
||||
return unicode(d, 'utf-8')
|
||||
return unicode(d, encoding)
|
||||
elif isinstance(d, dict):
|
||||
return dict(map(json_dict_bytes_to_unicode, d.iteritems()))
|
||||
return dict(imap(json_dict_bytes_to_unicode, d.iteritems(), repeat(encoding)))
|
||||
elif isinstance(d, list):
|
||||
return list(map(json_dict_bytes_to_unicode, d))
|
||||
return list(imap(json_dict_bytes_to_unicode, d, repeat(encoding)))
|
||||
elif isinstance(d, tuple):
|
||||
return tuple(map(json_dict_bytes_to_unicode, d))
|
||||
return tuple(imap(json_dict_bytes_to_unicode, d, repeat(encoding)))
|
||||
else:
|
||||
return d
|
||||
|
||||
@@ -363,9 +364,9 @@ class AnsibleModule(object):
|
||||
# reset to LANG=C if it's an invalid/unavailable locale
|
||||
self._check_locale()
|
||||
|
||||
(self.params, self.args) = self._load_params()
|
||||
self.params = self._load_params()
|
||||
|
||||
self._legal_inputs = ['CHECKMODE', 'NO_LOG']
|
||||
self._legal_inputs = ['_ansible_check_mode', '_ansible_no_log']
|
||||
|
||||
self.aliases = self._handle_aliases()
|
||||
|
||||
@@ -579,7 +580,7 @@ class AnsibleModule(object):
|
||||
if len(context) > i:
|
||||
if context[i] is not None and context[i] != cur_context[i]:
|
||||
new_context[i] = context[i]
|
||||
if context[i] is None:
|
||||
elif context[i] is None:
|
||||
new_context[i] = cur_context[i]
|
||||
|
||||
if cur_context != new_context:
|
||||
@@ -898,7 +899,7 @@ class AnsibleModule(object):
|
||||
|
||||
def _check_for_check_mode(self):
|
||||
for (k,v) in self.params.iteritems():
|
||||
if k == 'CHECKMODE':
|
||||
if k == '_ansible_check_mode':
|
||||
if not self.supports_check_mode:
|
||||
self.exit_json(skipped=True, msg="remote module does not support check mode")
|
||||
if self.supports_check_mode:
|
||||
@@ -906,13 +907,13 @@ class AnsibleModule(object):
|
||||
|
||||
def _check_for_no_log(self):
|
||||
for (k,v) in self.params.iteritems():
|
||||
if k == 'NO_LOG':
|
||||
if k == '_ansible_no_log':
|
||||
self.no_log = self.boolean(v)
|
||||
|
||||
def _check_invalid_arguments(self):
|
||||
for (k,v) in self.params.iteritems():
|
||||
# these should be in legal inputs already
|
||||
#if k in ('CHECKMODE', 'NO_LOG'):
|
||||
#if k in ('_ansible_check_mode', '_ansible_no_log'):
|
||||
# continue
|
||||
if k not in self._legal_inputs:
|
||||
self.fail_json(msg="unsupported parameter for module: %s" % k)
|
||||
@@ -930,7 +931,7 @@ class AnsibleModule(object):
|
||||
for check in spec:
|
||||
count = self._count_terms(check)
|
||||
if count > 1:
|
||||
self.fail_json(msg="parameters are mutually exclusive: %s" % check)
|
||||
self.fail_json(msg="parameters are mutually exclusive: %s" % (check,))
|
||||
|
||||
def _check_required_one_of(self, spec):
|
||||
if spec is None:
|
||||
@@ -948,7 +949,7 @@ class AnsibleModule(object):
|
||||
non_zero = [ c for c in counts if c > 0 ]
|
||||
if len(non_zero) > 0:
|
||||
if 0 in counts:
|
||||
self.fail_json(msg="parameters are required together: %s" % check)
|
||||
self.fail_json(msg="parameters are required together: %s" % (check,))
|
||||
|
||||
def _check_required_arguments(self):
|
||||
''' ensure all required arguments are present '''
|
||||
@@ -1102,20 +1103,11 @@ class AnsibleModule(object):
|
||||
|
||||
def _load_params(self):
|
||||
''' read the input and return a dictionary and the arguments string '''
|
||||
args = MODULE_ARGS
|
||||
items = shlex.split(args)
|
||||
params = {}
|
||||
for x in items:
|
||||
try:
|
||||
(k, v) = x.split("=",1)
|
||||
except Exception, e:
|
||||
self.fail_json(msg="this module requires key=value arguments (%s)" % (items))
|
||||
if k in params:
|
||||
self.fail_json(msg="duplicate parameter: %s (value=%s)" % (k, v))
|
||||
params[k] = v
|
||||
params2 = json_dict_unicode_to_bytes(json.loads(MODULE_COMPLEX_ARGS))
|
||||
params2.update(params)
|
||||
return (params2, args)
|
||||
params = json_dict_unicode_to_bytes(json.loads(MODULE_COMPLEX_ARGS))
|
||||
if params is None:
|
||||
params = dict()
|
||||
return params
|
||||
|
||||
|
||||
def _log_invocation(self):
|
||||
''' log that ansible ran the module '''
|
||||
@@ -1236,13 +1228,17 @@ class AnsibleModule(object):
|
||||
self.fail_json(msg='Boolean %s not in either boolean list' % arg)
|
||||
|
||||
def jsonify(self, data):
|
||||
for encoding in ("utf-8", "latin-1", "unicode_escape"):
|
||||
for encoding in ("utf-8", "latin-1"):
|
||||
try:
|
||||
return json.dumps(data, encoding=encoding)
|
||||
# Old systems using simplejson module does not support encoding keyword.
|
||||
except TypeError, e:
|
||||
return json.dumps(data)
|
||||
except UnicodeDecodeError, e:
|
||||
# Old systems using old simplejson module does not support encoding keyword.
|
||||
except TypeError:
|
||||
try:
|
||||
new_data = json_dict_bytes_to_unicode(data, encoding=encoding)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return json.dumps(new_data)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
self.fail_json(msg='Invalid unicode encoding encountered')
|
||||
|
||||
@@ -1479,7 +1475,7 @@ class AnsibleModule(object):
|
||||
msg = None
|
||||
st_in = None
|
||||
|
||||
# Set a temporart env path if a prefix is passed
|
||||
# Set a temporary env path if a prefix is passed
|
||||
env=os.environ
|
||||
if path_prefix:
|
||||
env['PATH']="%s:%s" % (path_prefix, env['PATH'])
|
||||
|
||||
@@ -471,29 +471,19 @@ class Facts(object):
|
||||
pass
|
||||
|
||||
def get_public_ssh_host_keys(self):
|
||||
dsa_filename = '/etc/ssh/ssh_host_dsa_key.pub'
|
||||
rsa_filename = '/etc/ssh/ssh_host_rsa_key.pub'
|
||||
ecdsa_filename = '/etc/ssh/ssh_host_ecdsa_key.pub'
|
||||
keytypes = ('dsa', 'rsa', 'ecdsa', 'ed25519')
|
||||
|
||||
if self.facts['system'] == 'Darwin':
|
||||
dsa_filename = '/etc/ssh_host_dsa_key.pub'
|
||||
rsa_filename = '/etc/ssh_host_rsa_key.pub'
|
||||
ecdsa_filename = '/etc/ssh_host_ecdsa_key.pub'
|
||||
dsa = get_file_content(dsa_filename)
|
||||
rsa = get_file_content(rsa_filename)
|
||||
ecdsa = get_file_content(ecdsa_filename)
|
||||
if dsa is None:
|
||||
dsa = 'NA'
|
||||
keydir = '/etc'
|
||||
else:
|
||||
self.facts['ssh_host_key_dsa_public'] = dsa.split()[1]
|
||||
if rsa is None:
|
||||
rsa = 'NA'
|
||||
else:
|
||||
self.facts['ssh_host_key_rsa_public'] = rsa.split()[1]
|
||||
if ecdsa is None:
|
||||
ecdsa = 'NA'
|
||||
else:
|
||||
self.facts['ssh_host_key_ecdsa_public'] = ecdsa.split()[1]
|
||||
keydir = '/etc/ssh'
|
||||
|
||||
for type_ in keytypes:
|
||||
key_filename = '%s/ssh_host_%s_key.pub' % (keydir, type_)
|
||||
keydata = get_file_content(key_filename)
|
||||
if keydata is not None:
|
||||
factname = 'ssh_host_key_%s_public' % type_
|
||||
self.facts[factname] = keydata.split()[1]
|
||||
|
||||
def get_pkg_mgr_facts(self):
|
||||
self.facts['pkg_mgr'] = 'unknown'
|
||||
|
||||
@@ -142,14 +142,14 @@ Function ConvertTo-Bool
|
||||
return
|
||||
}
|
||||
|
||||
# Helper function to calculate a hash of a file in a way which powershell 3
|
||||
# Helper function to calculate md5 of a file in a way which powershell 3
|
||||
# and above can handle:
|
||||
Function Get-FileChecksum($path)
|
||||
Function Get-FileMd5($path)
|
||||
{
|
||||
$hash = ""
|
||||
If (Test-Path -PathType Leaf $path)
|
||||
{
|
||||
$sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider;
|
||||
$sp = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider;
|
||||
$fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read);
|
||||
[System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
|
||||
$fp.Dispose();
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Joseph Callen <jcallen () csc.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/>.
|
||||
|
||||
|
||||
try:
|
||||
import atexit
|
||||
import time
|
||||
# requests is required for exception handling of the ConnectionError
|
||||
import requests
|
||||
from pyVim import connect
|
||||
from pyVmomi import vim, vmodl
|
||||
HAS_PYVMOMI = True
|
||||
except ImportError:
|
||||
HAS_PYVMOMI = False
|
||||
|
||||
|
||||
class TaskError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def wait_for_task(task):
|
||||
|
||||
while True:
|
||||
if task.info.state == vim.TaskInfo.State.success:
|
||||
return True, task.info.result
|
||||
if task.info.state == vim.TaskInfo.State.error:
|
||||
try:
|
||||
raise TaskError(task.info.error)
|
||||
except AttributeError:
|
||||
raise TaskError("An unknown error has occurred")
|
||||
if task.info.state == vim.TaskInfo.State.running:
|
||||
time.sleep(15)
|
||||
if task.info.state == vim.TaskInfo.State.queued:
|
||||
time.sleep(15)
|
||||
|
||||
|
||||
def find_dvspg_by_name(dv_switch, portgroup_name):
|
||||
|
||||
portgroups = dv_switch.portgroup
|
||||
|
||||
for pg in portgroups:
|
||||
if pg.name == portgroup_name:
|
||||
return pg
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_cluster_by_name_datacenter(datacenter, cluster_name):
|
||||
|
||||
host_folder = datacenter.hostFolder
|
||||
for folder in host_folder.childEntity:
|
||||
if folder.name == cluster_name:
|
||||
return folder
|
||||
return None
|
||||
|
||||
|
||||
def find_datacenter_by_name(content, datacenter_name):
|
||||
|
||||
datacenters = get_all_objs(content, [vim.Datacenter])
|
||||
for dc in datacenters:
|
||||
if dc.name == datacenter_name:
|
||||
return dc
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_dvs_by_name(content, switch_name):
|
||||
|
||||
vmware_distributed_switches = get_all_objs(content, [vim.dvs.VmwareDistributedVirtualSwitch])
|
||||
for dvs in vmware_distributed_switches:
|
||||
if dvs.name == switch_name:
|
||||
return dvs
|
||||
return None
|
||||
|
||||
|
||||
def find_hostsystem_by_name(content, hostname):
|
||||
|
||||
host_system = get_all_objs(content, [vim.HostSystem])
|
||||
for host in host_system:
|
||||
if host.name == hostname:
|
||||
return host
|
||||
return None
|
||||
|
||||
|
||||
def vmware_argument_spec():
|
||||
|
||||
return dict(
|
||||
hostname=dict(type='str', required=True),
|
||||
username=dict(type='str', aliases=['user', 'admin'], required=True),
|
||||
password=dict(type='str', aliases=['pass', 'pwd'], required=True, no_log=True),
|
||||
)
|
||||
|
||||
|
||||
def connect_to_api(module, disconnect_atexit=True):
|
||||
|
||||
hostname = module.params['hostname']
|
||||
username = module.params['username']
|
||||
password = module.params['password']
|
||||
try:
|
||||
service_instance = connect.SmartConnect(host=hostname, user=username, pwd=password)
|
||||
|
||||
# Disabling atexit should be used in special cases only.
|
||||
# Such as IP change of the ESXi host which removes the connection anyway.
|
||||
# Also removal significantly speeds up the return of the module
|
||||
|
||||
if disconnect_atexit:
|
||||
atexit.register(connect.Disconnect, service_instance)
|
||||
return service_instance.RetrieveContent()
|
||||
except vim.fault.InvalidLogin as invalid_login:
|
||||
module.fail_json(msg=invalid_login.msg, apierror=str(invalid_login))
|
||||
except requests.ConnectionError as connection_error:
|
||||
module.fail_json(msg="Unable to connect to vCenter or ESXi API on TCP/443.", apierror=str(connection_error))
|
||||
|
||||
|
||||
def get_all_objs(content, vimtype):
|
||||
|
||||
obj = {}
|
||||
container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True)
|
||||
for managed_object_ref in container.view:
|
||||
obj.update({managed_object_ref: managed_object_ref.name})
|
||||
return obj
|
||||
@@ -0,0 +1,20 @@
|
||||
# (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
|
||||
|
||||
Submodule lib/ansible/modules/core updated: 2b5e932cfb...9cc23c749a
341
lib/ansible/new_inventory/__init__.py
Normal file
341
lib/ansible/new_inventory/__init__.py
Normal file
@@ -0,0 +1,341 @@
|
||||
# (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 import constants as C
|
||||
from ansible.inventory.group import Group
|
||||
from .host import Host
|
||||
from ansible.plugins.inventory.aggregate import InventoryAggregateParser
|
||||
|
||||
class Inventory:
|
||||
'''
|
||||
Create hosts and groups from inventory
|
||||
|
||||
Retrieve the hosts and groups that ansible knows about from this class.
|
||||
|
||||
Retrieve raw variables (non-expanded) from the Group and Host classes
|
||||
returned from here.
|
||||
'''
|
||||
|
||||
def __init__(self, inventory_list=C.DEFAULT_HOST_LIST):
|
||||
'''
|
||||
:kwarg inventory_list: A list of inventory sources. This may be file
|
||||
names which will be parsed as ini-like files, executable scripts
|
||||
which return inventory data as json, directories of both of the above,
|
||||
or hostnames. Files and directories are
|
||||
:kwarg vault_password: Password to use if any of the inventory sources
|
||||
are in an ansible vault
|
||||
'''
|
||||
|
||||
self._restricted_to = None
|
||||
self._filter_pattern = None
|
||||
|
||||
parser = InventoryAggregateParser(inventory_list)
|
||||
parser.parse()
|
||||
|
||||
self._basedir = parser.basedir
|
||||
self._hosts = parser.hosts
|
||||
self._groups = parser.groups
|
||||
|
||||
def get_hosts(self):
|
||||
'''
|
||||
Return the list of hosts, after filtering based on any set pattern
|
||||
and restricting the results based on the set host restrictions.
|
||||
'''
|
||||
|
||||
if self._filter_pattern:
|
||||
hosts = self._filter_hosts()
|
||||
else:
|
||||
hosts = self._hosts[:]
|
||||
|
||||
if self._restricted_to is not None:
|
||||
# this will preserve the order of hosts after intersecting them
|
||||
res_set = set(hosts).intersection(self._restricted_to)
|
||||
return [h for h in hosts if h in res_set]
|
||||
else:
|
||||
return hosts[:]
|
||||
|
||||
def get_groups(self):
|
||||
'''
|
||||
Retrieve the Group objects known to the Inventory
|
||||
'''
|
||||
|
||||
return self._groups[:]
|
||||
|
||||
def get_host(self, hostname):
|
||||
'''
|
||||
Retrieve the Host object for a hostname
|
||||
'''
|
||||
|
||||
for host in self._hosts:
|
||||
if host.name == hostname:
|
||||
return host
|
||||
|
||||
return None
|
||||
|
||||
def get_group(self, groupname):
|
||||
'''
|
||||
Retrieve the Group object for a groupname
|
||||
'''
|
||||
|
||||
for group in self._groups:
|
||||
if group.name == group_name:
|
||||
return group
|
||||
|
||||
return None
|
||||
|
||||
def add_group(self, group):
|
||||
'''
|
||||
Add a new group to the inventory
|
||||
'''
|
||||
|
||||
if group not in self._groups:
|
||||
self._groups.append(group)
|
||||
|
||||
def set_filter_pattern(self, pattern='all'):
|
||||
'''
|
||||
Sets a pattern upon which hosts/groups will be filtered.
|
||||
This pattern can contain logical groupings such as unions,
|
||||
intersections and negations using special syntax.
|
||||
'''
|
||||
|
||||
self._filter_pattern = pattern
|
||||
|
||||
def set_host_restriction(self, restriction):
|
||||
'''
|
||||
Restrict operations to hosts in the given list
|
||||
'''
|
||||
|
||||
assert isinstance(restriction, list)
|
||||
self._restricted_to = restriction[:]
|
||||
|
||||
def remove_host_restriction(self):
|
||||
'''
|
||||
Remove the restriction on hosts, if any.
|
||||
'''
|
||||
|
||||
self._restricted_to = None
|
||||
|
||||
def _filter_hosts(self):
|
||||
"""
|
||||
Limits inventory results to a subset of inventory that matches a given
|
||||
list of patterns, such as to select a subset of a hosts selection that also
|
||||
belongs to a certain geographic group or numeric slice.
|
||||
|
||||
Corresponds to --limit parameter to ansible-playbook
|
||||
|
||||
:arg patterns: The pattern to limit with. If this is None it
|
||||
clears the subset. Multiple patterns may be specified as a comma,
|
||||
semicolon, or colon separated string.
|
||||
"""
|
||||
|
||||
hosts = []
|
||||
|
||||
pattern_regular = []
|
||||
pattern_intersection = []
|
||||
pattern_exclude = []
|
||||
|
||||
patterns = self._pattern.replace(";",":").split(":")
|
||||
for p in patterns:
|
||||
if p.startswith("!"):
|
||||
pattern_exclude.append(p)
|
||||
elif p.startswith("&"):
|
||||
pattern_intersection.append(p)
|
||||
elif p:
|
||||
pattern_regular.append(p)
|
||||
|
||||
# if no regular pattern was given, hence only exclude and/or intersection
|
||||
# make that magically work
|
||||
if pattern_regular == []:
|
||||
pattern_regular = ['all']
|
||||
|
||||
# when applying the host selectors, run those without the "&" or "!"
|
||||
# first, then the &s, then the !s.
|
||||
patterns = pattern_regular + pattern_intersection + pattern_exclude
|
||||
|
||||
for p in patterns:
|
||||
intersect = False
|
||||
negate = False
|
||||
if p.startswith('&'):
|
||||
intersect = True
|
||||
elif p.startswith('!'):
|
||||
p = p[1:]
|
||||
negate = True
|
||||
|
||||
target = self._resolve_pattern(p)
|
||||
if isinstance(target, Host):
|
||||
if negate and target in hosts:
|
||||
# remove it
|
||||
hosts.remove(target)
|
||||
elif target not in hosts:
|
||||
# for both union and intersections, we just append it
|
||||
hosts.append(target)
|
||||
else:
|
||||
if intersect:
|
||||
hosts = [ h for h in hosts if h not in target ]
|
||||
elif negate:
|
||||
hosts = [ h for h in hosts if h in target ]
|
||||
else:
|
||||
to_append = [ h for h in target if h.name not in [ y.name for y in hosts ] ]
|
||||
hosts.extend(to_append)
|
||||
|
||||
return hosts
|
||||
|
||||
def _resolve_pattern(self, pattern):
|
||||
target = self.get_host(pattern)
|
||||
if target:
|
||||
return target
|
||||
else:
|
||||
(name, enumeration_details) = self._enumeration_info(pattern)
|
||||
hpat = self._hosts_in_unenumerated_pattern(name)
|
||||
result = self._apply_ranges(pattern, hpat)
|
||||
return result
|
||||
|
||||
def _enumeration_info(self, pattern):
|
||||
"""
|
||||
returns (pattern, limits) taking a regular pattern and finding out
|
||||
which parts of it correspond to start/stop offsets. limits is
|
||||
a tuple of (start, stop) or None
|
||||
"""
|
||||
|
||||
# Do not parse regexes for enumeration info
|
||||
if pattern.startswith('~'):
|
||||
return (pattern, None)
|
||||
|
||||
# The regex used to match on the range, which can be [x] or [x-y].
|
||||
pattern_re = re.compile("^(.*)\[([-]?[0-9]+)(?:(?:-)([0-9]+))?\](.*)$")
|
||||
m = pattern_re.match(pattern)
|
||||
if m:
|
||||
(target, first, last, rest) = m.groups()
|
||||
first = int(first)
|
||||
if last:
|
||||
if first < 0:
|
||||
raise errors.AnsibleError("invalid range: negative indices cannot be used as the first item in a range")
|
||||
last = int(last)
|
||||
else:
|
||||
last = first
|
||||
return (target, (first, last))
|
||||
else:
|
||||
return (pattern, None)
|
||||
|
||||
def _apply_ranges(self, pat, hosts):
|
||||
"""
|
||||
given a pattern like foo, that matches hosts, return all of hosts
|
||||
given a pattern like foo[0:5], where foo matches hosts, return the first 6 hosts
|
||||
"""
|
||||
|
||||
# If there are no hosts to select from, just return the
|
||||
# empty set. This prevents trying to do selections on an empty set.
|
||||
# issue#6258
|
||||
if not hosts:
|
||||
return hosts
|
||||
|
||||
(loose_pattern, limits) = self._enumeration_info(pat)
|
||||
if not limits:
|
||||
return hosts
|
||||
|
||||
(left, right) = limits
|
||||
|
||||
if left == '':
|
||||
left = 0
|
||||
if right == '':
|
||||
right = 0
|
||||
left=int(left)
|
||||
right=int(right)
|
||||
try:
|
||||
if left != right:
|
||||
return hosts[left:right]
|
||||
else:
|
||||
return [ hosts[left] ]
|
||||
except IndexError:
|
||||
raise errors.AnsibleError("no hosts matching the pattern '%s' were found" % pat)
|
||||
|
||||
def _hosts_in_unenumerated_pattern(self, pattern):
|
||||
""" Get all host names matching the pattern """
|
||||
|
||||
results = []
|
||||
hosts = []
|
||||
hostnames = set()
|
||||
|
||||
# ignore any negative checks here, this is handled elsewhere
|
||||
pattern = pattern.replace("!","").replace("&", "")
|
||||
|
||||
def __append_host_to_results(host):
|
||||
if host not in results and host.name not in hostnames:
|
||||
hostnames.add(host.name)
|
||||
results.append(host)
|
||||
|
||||
groups = self.get_groups()
|
||||
for group in groups:
|
||||
if pattern == 'all':
|
||||
for host in group.get_hosts():
|
||||
__append_host_to_results(host)
|
||||
else:
|
||||
if self._match(group.name, pattern):
|
||||
for host in group.get_hosts():
|
||||
__append_host_to_results(host)
|
||||
else:
|
||||
matching_hosts = self._match_list(group.get_hosts(), 'name', pattern)
|
||||
for host in matching_hosts:
|
||||
__append_host_to_results(host)
|
||||
|
||||
if pattern in ["localhost", "127.0.0.1"] and len(results) == 0:
|
||||
new_host = self._create_implicit_localhost(pattern)
|
||||
results.append(new_host)
|
||||
return results
|
||||
|
||||
def _create_implicit_localhost(self, pattern):
|
||||
new_host = Host(pattern)
|
||||
new_host._connection = 'local'
|
||||
new_host.set_variable("ansible_python_interpreter", sys.executable)
|
||||
ungrouped = self.get_group("ungrouped")
|
||||
if ungrouped is None:
|
||||
self.add_group(Group('ungrouped'))
|
||||
ungrouped = self.get_group('ungrouped')
|
||||
self.get_group('all').add_child_group(ungrouped)
|
||||
ungrouped.add_host(new_host)
|
||||
return new_host
|
||||
|
||||
def is_file(self):
|
||||
'''
|
||||
Did inventory come from a file?
|
||||
|
||||
:returns: True if the inventory is file based, False otherwise
|
||||
'''
|
||||
pass
|
||||
|
||||
def src(self):
|
||||
'''
|
||||
What's the complete path to the inventory file?
|
||||
|
||||
:returns: Complete path to the inventory file. None if inventory is
|
||||
not file-based
|
||||
'''
|
||||
pass
|
||||
|
||||
def basedir(self):
|
||||
'''
|
||||
What directory from which the inventory was read.
|
||||
'''
|
||||
|
||||
return self._basedir
|
||||
|
||||
21
lib/ansible/new_inventory/group.py
Normal file
21
lib/ansible/new_inventory/group.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
|
||||
|
||||
51
lib/ansible/new_inventory/host.py
Normal file
51
lib/ansible/new_inventory/host.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# (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
|
||||
|
||||
class Host:
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._connection = None
|
||||
self._ipv4_address = ''
|
||||
self._ipv6_address = ''
|
||||
self._port = 22
|
||||
self._vars = dict()
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_name()
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def get_groups(self):
|
||||
return []
|
||||
|
||||
def set_variable(self, name, value):
|
||||
''' sets a variable for this host '''
|
||||
|
||||
self._vars[name] = value
|
||||
|
||||
def get_vars(self):
|
||||
''' returns all variables for this host '''
|
||||
|
||||
all_vars = self._vars.copy()
|
||||
all_vars.update(dict(inventory_hostname=self._name))
|
||||
return all_vars
|
||||
|
||||
222
lib/ansible/parsing/__init__.py
Normal file
222
lib/ansible/parsing/__init__.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# (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
|
||||
import os
|
||||
|
||||
from yaml import load, YAMLError
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.errors.yaml_strings import YAML_SYNTAX_ERROR
|
||||
from ansible.parsing.vault import VaultLib
|
||||
from ansible.parsing.splitter import unquote
|
||||
from ansible.parsing.yaml.loader import AnsibleLoader
|
||||
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleUnicode
|
||||
from ansible.utils.path import unfrackpath
|
||||
|
||||
class DataLoader():
|
||||
|
||||
'''
|
||||
The DataLoader class is used to load and parse YAML or JSON content,
|
||||
either from a given file name or from a string that was previously
|
||||
read in through other means. A Vault password can be specified, and
|
||||
any vault-encrypted files will be decrypted.
|
||||
|
||||
Data read from files will also be cached, so the file will never be
|
||||
read from disk more than once.
|
||||
|
||||
Usage:
|
||||
|
||||
dl = DataLoader()
|
||||
(or)
|
||||
dl = DataLoader(vault_password='foo')
|
||||
|
||||
ds = dl.load('...')
|
||||
ds = dl.load_from_file('/path/to/file')
|
||||
'''
|
||||
|
||||
def __init__(self, vault_password=None):
|
||||
self._basedir = '.'
|
||||
self._vault_password = vault_password
|
||||
self._FILE_CACHE = dict()
|
||||
|
||||
self._vault = VaultLib(password=vault_password)
|
||||
|
||||
def load(self, data, file_name='<string>', show_content=True):
|
||||
'''
|
||||
Creates a python datastructure from the given data, which can be either
|
||||
a JSON or YAML string.
|
||||
'''
|
||||
|
||||
try:
|
||||
# we first try to load this data as JSON
|
||||
return json.loads(data)
|
||||
except:
|
||||
# if loading JSON failed for any reason, we go ahead
|
||||
# and try to parse it as YAML instead
|
||||
|
||||
if isinstance(data, AnsibleUnicode):
|
||||
# The PyYAML's libyaml bindings use PyUnicode_CheckExact so
|
||||
# they are unable to cope with our subclass.
|
||||
# Unwrap and re-wrap the unicode so we can keep track of line
|
||||
# numbers
|
||||
new_data = unicode(data)
|
||||
else:
|
||||
new_data = data
|
||||
try:
|
||||
new_data = self._safe_load(new_data, file_name=file_name)
|
||||
except YAMLError as yaml_exc:
|
||||
self._handle_error(yaml_exc, file_name, show_content)
|
||||
|
||||
if isinstance(data, AnsibleUnicode):
|
||||
new_data = AnsibleUnicode(new_data)
|
||||
new_data.ansible_pos = data.ansible_pos
|
||||
return new_data
|
||||
|
||||
def load_from_file(self, file_name):
|
||||
''' Loads data from a file, which can contain either JSON or YAML. '''
|
||||
|
||||
file_name = self.path_dwim(file_name)
|
||||
|
||||
# if the file has already been read in and cached, we'll
|
||||
# return those results to avoid more file/vault operations
|
||||
if file_name in self._FILE_CACHE:
|
||||
return self._FILE_CACHE[file_name]
|
||||
|
||||
# read the file contents and load the data structure from them
|
||||
(file_data, show_content) = self._get_file_contents(file_name)
|
||||
parsed_data = self.load(data=file_data, file_name=file_name, show_content=show_content)
|
||||
|
||||
# cache the file contents for next time
|
||||
self._FILE_CACHE[file_name] = parsed_data
|
||||
|
||||
return parsed_data
|
||||
|
||||
def path_exists(self, path):
|
||||
return os.path.exists(path)
|
||||
|
||||
def is_file(self, path):
|
||||
return os.path.isfile(path)
|
||||
|
||||
def is_directory(self, path):
|
||||
return os.path.isdir(path)
|
||||
|
||||
def list_directory(self, path):
|
||||
return os.listdir(path)
|
||||
|
||||
def _safe_load(self, stream, file_name=None):
|
||||
''' Implements yaml.safe_load(), except using our custom loader class. '''
|
||||
|
||||
loader = AnsibleLoader(stream, file_name)
|
||||
try:
|
||||
return loader.get_single_data()
|
||||
finally:
|
||||
loader.dispose()
|
||||
|
||||
def _get_file_contents(self, file_name):
|
||||
'''
|
||||
Reads the file contents from the given file name, and will decrypt them
|
||||
if they are found to be vault-encrypted.
|
||||
'''
|
||||
|
||||
if not self.path_exists(file_name) or not self.is_file(file_name):
|
||||
raise AnsibleParserError("the file_name '%s' does not exist, or is not readable" % file_name)
|
||||
|
||||
show_content = True
|
||||
try:
|
||||
with open(file_name, 'r') as f:
|
||||
data = f.read()
|
||||
if self._vault.is_encrypted(data):
|
||||
data = self._vault.decrypt(data)
|
||||
show_content = False
|
||||
return (data, show_content)
|
||||
except (IOError, OSError) as e:
|
||||
raise AnsibleParserError("an error occurred while trying to read the file '%s': %s" % (file_name, str(e)))
|
||||
|
||||
def _handle_error(self, yaml_exc, file_name, show_content):
|
||||
'''
|
||||
Optionally constructs an object (AnsibleBaseYAMLObject) to encapsulate the
|
||||
file name/position where a YAML exception occurred, and raises an AnsibleParserError
|
||||
to display the syntax exception information.
|
||||
'''
|
||||
|
||||
# if the YAML exception contains a problem mark, use it to construct
|
||||
# an object the error class can use to display the faulty line
|
||||
err_obj = None
|
||||
if hasattr(yaml_exc, 'problem_mark'):
|
||||
err_obj = AnsibleBaseYAMLObject()
|
||||
err_obj.ansible_pos = (file_name, yaml_exc.problem_mark.line + 1, yaml_exc.problem_mark.column + 1)
|
||||
|
||||
raise AnsibleParserError(YAML_SYNTAX_ERROR, obj=err_obj, show_content=show_content)
|
||||
|
||||
def get_basedir(self):
|
||||
''' returns the current basedir '''
|
||||
return self._basedir
|
||||
|
||||
def set_basedir(self, basedir):
|
||||
''' sets the base directory, used to find files when a relative path is given '''
|
||||
|
||||
if basedir is not None:
|
||||
self._basedir = basedir
|
||||
|
||||
def path_dwim(self, given):
|
||||
'''
|
||||
make relative paths work like folks expect.
|
||||
'''
|
||||
|
||||
given = unquote(given)
|
||||
|
||||
if given.startswith("/"):
|
||||
return os.path.abspath(given)
|
||||
elif given.startswith("~"):
|
||||
return os.path.abspath(os.path.expanduser(given))
|
||||
else:
|
||||
return os.path.abspath(os.path.join(self._basedir, given))
|
||||
|
||||
def path_dwim_relative(self, role_path, dirname, source):
|
||||
''' find one file in a directory one level up in a dir named dirname relative to current '''
|
||||
|
||||
basedir = os.path.dirname(role_path)
|
||||
if os.path.islink(basedir):
|
||||
basedir = unfrackpath(basedir)
|
||||
template2 = os.path.join(basedir, dirname, source)
|
||||
else:
|
||||
template2 = os.path.join(basedir, '..', dirname, source)
|
||||
|
||||
source1 = os.path.join(role_path, dirname, source)
|
||||
if os.path.exists(source1):
|
||||
return source1
|
||||
|
||||
cur_basedir = self._basedir
|
||||
self.set_basedir(basedir)
|
||||
source2 = self.path_dwim(template2)
|
||||
if os.path.exists(source2):
|
||||
self.set_basedir(cur_basedir)
|
||||
return source2
|
||||
|
||||
obvious_local_path = self.path_dwim(source)
|
||||
if os.path.exists(obvious_local_path):
|
||||
self.set_basedir(cur_basedir)
|
||||
return obvious_local_path
|
||||
|
||||
self.set_basedir(cur_basedir)
|
||||
return source2 # which does not exist
|
||||
|
||||
288
lib/ansible/parsing/mod_args.py
Normal file
288
lib/ansible/parsing/mod_args.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# (c) 2014 Michael DeHaan, <michael@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 six import iteritems, string_types
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.plugins import module_loader
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
|
||||
class ModuleArgsParser:
|
||||
|
||||
"""
|
||||
There are several ways a module and argument set can be expressed:
|
||||
|
||||
# legacy form (for a shell command)
|
||||
- action: shell echo hi
|
||||
|
||||
# common shorthand for local actions vs delegate_to
|
||||
- local_action: shell echo hi
|
||||
|
||||
# most commonly:
|
||||
- copy: src=a dest=b
|
||||
|
||||
# legacy form
|
||||
- action: copy src=a dest=b
|
||||
|
||||
# complex args form, for passing structured data
|
||||
- copy:
|
||||
src: a
|
||||
dest: b
|
||||
|
||||
# gross, but technically legal
|
||||
- action:
|
||||
module: copy
|
||||
args:
|
||||
src: a
|
||||
dest: b
|
||||
|
||||
# extra gross, but also legal. in this case, the args specified
|
||||
# will act as 'defaults' and will be overridden by any args specified
|
||||
# in one of the other formats (complex args under the action, or
|
||||
# parsed from the k=v string
|
||||
- command: 'pwd'
|
||||
args:
|
||||
chdir: '/tmp'
|
||||
|
||||
|
||||
This class has some of the logic to canonicalize these into the form
|
||||
|
||||
- module: <module_name>
|
||||
delegate_to: <optional>
|
||||
args: <args>
|
||||
|
||||
Args may also be munged for certain shell command parameters.
|
||||
"""
|
||||
|
||||
def __init__(self, task_ds=dict()):
|
||||
assert isinstance(task_ds, dict)
|
||||
self._task_ds = task_ds
|
||||
|
||||
|
||||
def _split_module_string(self, str):
|
||||
'''
|
||||
when module names are expressed like:
|
||||
action: copy src=a dest=b
|
||||
the first part of the string is the name of the module
|
||||
and the rest are strings pertaining to the arguments.
|
||||
'''
|
||||
|
||||
tokens = str.split()
|
||||
if len(tokens) > 1:
|
||||
return (tokens[0], " ".join(tokens[1:]))
|
||||
else:
|
||||
return (tokens[0], "")
|
||||
|
||||
|
||||
def _handle_shell_weirdness(self, action, args):
|
||||
'''
|
||||
given an action name and an args dictionary, return the
|
||||
proper action name and args dictionary. This mostly is due
|
||||
to shell/command being treated special and nothing else
|
||||
'''
|
||||
|
||||
# don't handle non shell/command modules in this function
|
||||
# TODO: in terms of the whole app, should 'raw' also fit here?
|
||||
if action not in ['shell', 'command']:
|
||||
return (action, args)
|
||||
|
||||
# the shell module really is the command module with an additional
|
||||
# parameter
|
||||
if action == 'shell':
|
||||
action = 'command'
|
||||
args['_uses_shell'] = True
|
||||
|
||||
return (action, args)
|
||||
|
||||
def _normalize_parameters(self, thing, action=None, additional_args=dict()):
|
||||
'''
|
||||
arguments can be fuzzy. Deal with all the forms.
|
||||
'''
|
||||
|
||||
# final args are the ones we'll eventually return, so first update
|
||||
# them with any additional args specified, which have lower priority
|
||||
# than those which may be parsed/normalized next
|
||||
final_args = dict()
|
||||
if additional_args:
|
||||
final_args.update(additional_args)
|
||||
|
||||
# how we normalize depends if we figured out what the module name is
|
||||
# yet. If we have already figured it out, it's an 'old style' invocation.
|
||||
# otherwise, it's not
|
||||
|
||||
if action is not None:
|
||||
args = self._normalize_old_style_args(thing, action)
|
||||
else:
|
||||
(action, args) = self._normalize_new_style_args(thing)
|
||||
|
||||
# this can occasionally happen, simplify
|
||||
if args and 'args' in args:
|
||||
tmp_args = args['args']
|
||||
del args['args']
|
||||
if isinstance(tmp_args, string_types):
|
||||
tmp_args = parse_kv(tmp_args)
|
||||
args.update(tmp_args)
|
||||
|
||||
# finally, update the args we're going to return with the ones
|
||||
# which were normalized above
|
||||
if args:
|
||||
final_args.update(args)
|
||||
|
||||
return (action, final_args)
|
||||
|
||||
def _normalize_old_style_args(self, thing, action):
|
||||
'''
|
||||
deals with fuzziness in old-style (action/local_action) module invocations
|
||||
returns tuple of (module_name, dictionary_args)
|
||||
|
||||
possible example inputs:
|
||||
{ 'local_action' : 'shell echo hi' }
|
||||
{ 'action' : 'shell echo hi' }
|
||||
{ 'local_action' : { 'module' : 'ec2', 'x' : 1, 'y': 2 }}
|
||||
standardized outputs like:
|
||||
( 'command', { _raw_params: 'echo hi', _uses_shell: True }
|
||||
'''
|
||||
|
||||
if isinstance(thing, dict):
|
||||
# form is like: local_action: { module: 'xyz', x: 2, y: 3 } ... uncommon!
|
||||
args = thing
|
||||
elif isinstance(thing, string_types):
|
||||
# form is like: local_action: copy src=a dest=b ... pretty common
|
||||
check_raw = action in ('command', 'shell', 'script')
|
||||
args = parse_kv(thing, check_raw=check_raw)
|
||||
elif thing is None:
|
||||
# this can happen with modules which take no params, like ping:
|
||||
args = None
|
||||
else:
|
||||
raise AnsibleParserError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds)
|
||||
return args
|
||||
|
||||
def _normalize_new_style_args(self, thing):
|
||||
'''
|
||||
deals with fuzziness in new style module invocations
|
||||
accepting key=value pairs and dictionaries, and always returning dictionaries
|
||||
returns tuple of (module_name, dictionary_args)
|
||||
|
||||
possible example inputs:
|
||||
{ 'shell' : 'echo hi' }
|
||||
{ 'ec2' : { 'region' : 'xyz' }
|
||||
{ 'ec2' : 'region=xyz' }
|
||||
standardized outputs like:
|
||||
('ec2', { region: 'xyz'} )
|
||||
'''
|
||||
|
||||
action = None
|
||||
args = None
|
||||
|
||||
if isinstance(thing, dict):
|
||||
# form is like: copy: { src: 'a', dest: 'b' } ... common for structured (aka "complex") args
|
||||
thing = thing.copy()
|
||||
if 'module' in thing:
|
||||
action = thing['module']
|
||||
args = thing.copy()
|
||||
del args['module']
|
||||
|
||||
elif isinstance(thing, string_types):
|
||||
# form is like: copy: src=a dest=b ... common shorthand throughout ansible
|
||||
(action, args) = self._split_module_string(thing)
|
||||
check_raw = action in ('command', 'shell', 'script')
|
||||
args = parse_kv(args, check_raw=check_raw)
|
||||
|
||||
else:
|
||||
# need a dict or a string, so giving up
|
||||
raise AnsibleParserError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds)
|
||||
|
||||
return (action, args)
|
||||
|
||||
def parse(self):
|
||||
'''
|
||||
Given a task in one of the supported forms, parses and returns
|
||||
returns the action, arguments, and delegate_to values for the
|
||||
task, dealing with all sorts of levels of fuzziness.
|
||||
'''
|
||||
|
||||
thing = None
|
||||
|
||||
action = None
|
||||
delegate_to = self._task_ds.get('delegate_to', None)
|
||||
args = dict()
|
||||
|
||||
|
||||
#
|
||||
# We can have one of action, local_action, or module specified
|
||||
#
|
||||
|
||||
|
||||
# this is the 'extra gross' scenario detailed above, so we grab
|
||||
# the args and pass them in as additional arguments, which can/will
|
||||
# be overwritten via dict updates from the other arg sources below
|
||||
# FIXME: add test cases for this
|
||||
additional_args = self._task_ds.get('args', dict())
|
||||
|
||||
# action
|
||||
if 'action' in self._task_ds:
|
||||
# an old school 'action' statement
|
||||
thing = self._task_ds['action']
|
||||
action, args = self._normalize_parameters(thing, additional_args=additional_args)
|
||||
|
||||
# local_action
|
||||
if 'local_action' in self._task_ds:
|
||||
# local_action is similar but also implies a delegate_to
|
||||
if action is not None:
|
||||
raise AnsibleParserError("action and local_action are mutually exclusive", obj=self._task_ds)
|
||||
thing = self._task_ds.get('local_action', '')
|
||||
delegate_to = 'localhost'
|
||||
action, args = self._normalize_parameters(thing, additional_args=additional_args)
|
||||
|
||||
# module: <stuff> is the more new-style invocation
|
||||
|
||||
# walk the input dictionary to see we recognize a module name
|
||||
for (item, value) in iteritems(self._task_ds):
|
||||
if item in module_loader or item == 'meta' or item == 'include':
|
||||
# finding more than one module name is a problem
|
||||
if action is not None:
|
||||
raise AnsibleParserError("conflicting action statements", obj=self._task_ds)
|
||||
action = item
|
||||
thing = value
|
||||
action, args = self._normalize_parameters(value, action=action, additional_args=additional_args)
|
||||
|
||||
# FIXME: this should probably be somewhere else
|
||||
RAW_PARAM_MODULES = (
|
||||
'command',
|
||||
'shell',
|
||||
'script',
|
||||
'include',
|
||||
'include_vars',
|
||||
'add_host',
|
||||
'group_by',
|
||||
'set_fact',
|
||||
'meta',
|
||||
)
|
||||
# if we didn't see any module in the task at all, it's not a task really
|
||||
if action is None:
|
||||
raise AnsibleParserError("no action detected in task", obj=self._task_ds)
|
||||
elif args.get('_raw_params', '') != '' and action not in RAW_PARAM_MODULES:
|
||||
raise AnsibleParserError("this task '%s' has extra params, which is only allowed in the following modules: %s" % (action, ", ".join(RAW_PARAM_MODULES)), obj=self._task_ds)
|
||||
|
||||
# shell modules require special handling
|
||||
(action, args) = self._handle_shell_weirdness(action, args)
|
||||
|
||||
return (action, args, delegate_to)
|
||||
273
lib/ansible/parsing/splitter.py
Normal file
273
lib/ansible/parsing/splitter.py
Normal file
@@ -0,0 +1,273 @@
|
||||
# (c) 2014 James Cammarata, <jcammarata@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
|
||||
|
||||
import re
|
||||
import codecs
|
||||
|
||||
# Decode escapes adapted from rspeer's answer here:
|
||||
# http://stackoverflow.com/questions/4020539/process-escape-sequences-in-a-string-in-python
|
||||
_HEXCHAR = '[a-fA-F0-9]'
|
||||
_ESCAPE_SEQUENCE_RE = re.compile(r'''
|
||||
( \\U{0} # 8-digit hex escapes
|
||||
| \\u{1} # 4-digit hex escapes
|
||||
| \\x{2} # 2-digit hex escapes
|
||||
| \\[0-7]{{1,3}} # Octal escapes
|
||||
| \\N\{{[^}}]+\}} # Unicode characters by name
|
||||
| \\[\\'"abfnrtv] # Single-character escapes
|
||||
)'''.format(_HEXCHAR*8, _HEXCHAR*4, _HEXCHAR*2), re.UNICODE | re.VERBOSE)
|
||||
|
||||
def _decode_escapes(s):
|
||||
def decode_match(match):
|
||||
return codecs.decode(match.group(0), 'unicode-escape')
|
||||
|
||||
return _ESCAPE_SEQUENCE_RE.sub(decode_match, s)
|
||||
|
||||
def parse_kv(args, check_raw=False):
|
||||
'''
|
||||
Convert a string of key/value items to a dict. If any free-form params
|
||||
are found and the check_raw option is set to True, they will be added
|
||||
to a new parameter called '_raw_params'. If check_raw is not enabled,
|
||||
they will simply be ignored.
|
||||
'''
|
||||
|
||||
### FIXME: args should already be a unicode string
|
||||
from ansible.utils.unicode import to_unicode
|
||||
args = to_unicode(args, nonstring='passthru')
|
||||
|
||||
options = {}
|
||||
if args is not None:
|
||||
try:
|
||||
vargs = split_args(args)
|
||||
except ValueError as ve:
|
||||
if 'no closing quotation' in str(ve).lower():
|
||||
raise errors.AnsibleError("error parsing argument string, try quoting the entire line.")
|
||||
else:
|
||||
raise
|
||||
|
||||
raw_params = []
|
||||
for x in vargs:
|
||||
x = _decode_escapes(x)
|
||||
if "=" in x:
|
||||
pos = 0
|
||||
try:
|
||||
while True:
|
||||
pos = x.index('=', pos + 1)
|
||||
if pos > 0 and x[pos - 1] != '\\':
|
||||
break
|
||||
except ValueError:
|
||||
# ran out of string, but we must have some escaped equals,
|
||||
# so replace those and append this to the list of raw params
|
||||
raw_params.append(x.replace('\\=', '='))
|
||||
continue
|
||||
|
||||
k = x[:pos]
|
||||
v = x[pos + 1:]
|
||||
|
||||
# only internal variables can start with an underscore, so
|
||||
# we don't allow users to set them directy in arguments
|
||||
if k.startswith('_'):
|
||||
raise AnsibleError("invalid parameter specified: '%s'" % k)
|
||||
|
||||
# FIXME: make the retrieval of this list of shell/command
|
||||
# options a function, so the list is centralized
|
||||
if check_raw and k not in ('creates', 'removes', 'chdir', 'executable', 'warn'):
|
||||
raw_params.append(x)
|
||||
else:
|
||||
options[k.strip()] = unquote(v.strip())
|
||||
else:
|
||||
raw_params.append(x)
|
||||
|
||||
# recombine the free-form params, if any were found, and assign
|
||||
# them to a special option for use later by the shell/command module
|
||||
if len(raw_params) > 0:
|
||||
options[u'_raw_params'] = ' '.join(raw_params)
|
||||
|
||||
return options
|
||||
|
||||
def _get_quote_state(token, quote_char):
|
||||
'''
|
||||
the goal of this block is to determine if the quoted string
|
||||
is unterminated in which case it needs to be put back together
|
||||
'''
|
||||
# the char before the current one, used to see if
|
||||
# the current character is escaped
|
||||
prev_char = None
|
||||
for idx, cur_char in enumerate(token):
|
||||
if idx > 0:
|
||||
prev_char = token[idx-1]
|
||||
if cur_char in '"\'' and prev_char != '\\':
|
||||
if quote_char:
|
||||
if cur_char == quote_char:
|
||||
quote_char = None
|
||||
else:
|
||||
quote_char = cur_char
|
||||
return quote_char
|
||||
|
||||
def _count_jinja2_blocks(token, cur_depth, open_token, close_token):
|
||||
'''
|
||||
this function counts the number of opening/closing blocks for a
|
||||
given opening/closing type and adjusts the current depth for that
|
||||
block based on the difference
|
||||
'''
|
||||
num_open = token.count(open_token)
|
||||
num_close = token.count(close_token)
|
||||
if num_open != num_close:
|
||||
cur_depth += (num_open - num_close)
|
||||
if cur_depth < 0:
|
||||
cur_depth = 0
|
||||
return cur_depth
|
||||
|
||||
def split_args(args):
|
||||
'''
|
||||
Splits args on whitespace, but intelligently reassembles
|
||||
those that may have been split over a jinja2 block or quotes.
|
||||
|
||||
When used in a remote module, we won't ever have to be concerned about
|
||||
jinja2 blocks, however this function is/will be used in the
|
||||
core portions as well before the args are templated.
|
||||
|
||||
example input: a=b c="foo bar"
|
||||
example output: ['a=b', 'c="foo bar"']
|
||||
|
||||
Basically this is a variation shlex that has some more intelligence for
|
||||
how Ansible needs to use it.
|
||||
'''
|
||||
|
||||
# the list of params parsed out of the arg string
|
||||
# this is going to be the result value when we are done
|
||||
params = []
|
||||
|
||||
# Initial split on white space
|
||||
args = args.strip()
|
||||
items = args.strip().split('\n')
|
||||
|
||||
# iterate over the tokens, and reassemble any that may have been
|
||||
# split on a space inside a jinja2 block.
|
||||
# ex if tokens are "{{", "foo", "}}" these go together
|
||||
|
||||
# These variables are used
|
||||
# to keep track of the state of the parsing, since blocks and quotes
|
||||
# may be nested within each other.
|
||||
|
||||
quote_char = None
|
||||
inside_quotes = False
|
||||
print_depth = 0 # used to count nested jinja2 {{ }} blocks
|
||||
block_depth = 0 # used to count nested jinja2 {% %} blocks
|
||||
comment_depth = 0 # used to count nested jinja2 {# #} blocks
|
||||
|
||||
# now we loop over each split chunk, coalescing tokens if the white space
|
||||
# split occurred within quotes or a jinja2 block of some kind
|
||||
for itemidx,item in enumerate(items):
|
||||
|
||||
# we split on spaces and newlines separately, so that we
|
||||
# can tell which character we split on for reassembly
|
||||
# inside quotation characters
|
||||
tokens = item.strip().split(' ')
|
||||
|
||||
line_continuation = False
|
||||
for idx,token in enumerate(tokens):
|
||||
|
||||
# if we hit a line continuation character, but
|
||||
# we're not inside quotes, ignore it and continue
|
||||
# on to the next token while setting a flag
|
||||
if token == '\\' and not inside_quotes:
|
||||
line_continuation = True
|
||||
continue
|
||||
|
||||
# store the previous quoting state for checking later
|
||||
was_inside_quotes = inside_quotes
|
||||
quote_char = _get_quote_state(token, quote_char)
|
||||
inside_quotes = quote_char is not None
|
||||
|
||||
# multiple conditions may append a token to the list of params,
|
||||
# so we keep track with this flag to make sure it only happens once
|
||||
# append means add to the end of the list, don't append means concatenate
|
||||
# it to the end of the last token
|
||||
appended = False
|
||||
|
||||
# if we're inside quotes now, but weren't before, append the token
|
||||
# to the end of the list, since we'll tack on more to it later
|
||||
# otherwise, if we're inside any jinja2 block, inside quotes, or we were
|
||||
# inside quotes (but aren't now) concat this token to the last param
|
||||
if inside_quotes and not was_inside_quotes:
|
||||
params.append(token)
|
||||
appended = True
|
||||
elif print_depth or block_depth or comment_depth or inside_quotes or was_inside_quotes:
|
||||
if idx == 0 and was_inside_quotes:
|
||||
params[-1] = "%s%s" % (params[-1], token)
|
||||
elif len(tokens) > 1:
|
||||
spacer = ''
|
||||
if idx > 0:
|
||||
spacer = ' '
|
||||
params[-1] = "%s%s%s" % (params[-1], spacer, token)
|
||||
else:
|
||||
params[-1] = "%s\n%s" % (params[-1], token)
|
||||
appended = True
|
||||
|
||||
# if the number of paired block tags is not the same, the depth has changed, so we calculate that here
|
||||
# and may append the current token to the params (if we haven't previously done so)
|
||||
prev_print_depth = print_depth
|
||||
print_depth = _count_jinja2_blocks(token, print_depth, "{{", "}}")
|
||||
if print_depth != prev_print_depth and not appended:
|
||||
params.append(token)
|
||||
appended = True
|
||||
|
||||
prev_block_depth = block_depth
|
||||
block_depth = _count_jinja2_blocks(token, block_depth, "{%", "%}")
|
||||
if block_depth != prev_block_depth and not appended:
|
||||
params.append(token)
|
||||
appended = True
|
||||
|
||||
prev_comment_depth = comment_depth
|
||||
comment_depth = _count_jinja2_blocks(token, comment_depth, "{#", "#}")
|
||||
if comment_depth != prev_comment_depth and not appended:
|
||||
params.append(token)
|
||||
appended = True
|
||||
|
||||
# finally, if we're at zero depth for all blocks and not inside quotes, and have not
|
||||
# yet appended anything to the list of params, we do so now
|
||||
if not (print_depth or block_depth or comment_depth) and not inside_quotes and not appended and token != '':
|
||||
params.append(token)
|
||||
|
||||
# if this was the last token in the list, and we have more than
|
||||
# one item (meaning we split on newlines), add a newline back here
|
||||
# to preserve the original structure
|
||||
if len(items) > 1 and itemidx != len(items) - 1 and not line_continuation:
|
||||
params[-1] += '\n'
|
||||
|
||||
# always clear the line continuation flag
|
||||
line_continuation = False
|
||||
|
||||
# If we're done and things are not at zero depth or we're still inside quotes,
|
||||
# raise an error to indicate that the args were unbalanced
|
||||
if print_depth or block_depth or comment_depth or inside_quotes:
|
||||
raise Exception("error while splitting arguments, either an unbalanced jinja2 block or quotes")
|
||||
|
||||
return params
|
||||
|
||||
def is_quoted(data):
|
||||
return len(data) > 0 and (data[0] == '"' and data[-1] == '"' or data[0] == "'" and data[-1] == "'")
|
||||
|
||||
def unquote(data):
|
||||
''' removes first and last quotes from a string, if the string starts and ends with the same quotes '''
|
||||
if is_quoted(data):
|
||||
return data[1:-1]
|
||||
return data
|
||||
21
lib/ansible/parsing/utils/__init__.py
Normal file
21
lib/ansible/parsing/utils/__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
|
||||
|
||||
45
lib/ansible/parsing/utils/jsonify.py
Normal file
45
lib/ansible/parsing/utils/jsonify.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# (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
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
def jsonify(result, format=False):
|
||||
''' format JSON output (uncompressed or uncompressed) '''
|
||||
|
||||
if result is None:
|
||||
return "{}"
|
||||
result2 = result.copy()
|
||||
for key, value in result2.items():
|
||||
if type(value) is str:
|
||||
result2[key] = value.decode('utf-8', 'ignore')
|
||||
|
||||
indent = None
|
||||
if format:
|
||||
indent = 4
|
||||
|
||||
try:
|
||||
return json.dumps(result2, sort_keys=True, indent=indent, ensure_ascii=False)
|
||||
except UnicodeDecodeError:
|
||||
return json.dumps(result2, sort_keys=True, indent=indent)
|
||||
|
||||
603
lib/ansible/parsing/vault/__init__.py
Normal file
603
lib/ansible/parsing/vault/__init__.py
Normal file
@@ -0,0 +1,603 @@
|
||||
# (c) 2014, James Tanner <tanner.jc@gmail.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/>.
|
||||
#
|
||||
# ansible-pull is a script that runs ansible in local mode
|
||||
# after checking out a playbooks directory from source repo. There is an
|
||||
# example playbook to bootstrap this script in the examples/ dir which
|
||||
# installs ansible and sets it up to run on cron.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import tempfile
|
||||
from io import BytesIO
|
||||
from subprocess import call
|
||||
from ansible import errors
|
||||
from hashlib import sha256
|
||||
# Note: Only used for loading obsolete VaultAES files. All files are written
|
||||
# using the newer VaultAES256 which does not require md5
|
||||
from hashlib import md5
|
||||
from binascii import hexlify
|
||||
from binascii import unhexlify
|
||||
from six import binary_type, byte2int, PY2, text_type
|
||||
from ansible import constants as C
|
||||
from ansible.utils.unicode import to_unicode, to_bytes
|
||||
|
||||
|
||||
try:
|
||||
from Crypto.Hash import SHA256, HMAC
|
||||
HAS_HASH = True
|
||||
except ImportError:
|
||||
HAS_HASH = False
|
||||
|
||||
# Counter import fails for 2.0.1, requires >= 2.6.1 from pip
|
||||
try:
|
||||
from Crypto.Util import Counter
|
||||
HAS_COUNTER = True
|
||||
except ImportError:
|
||||
HAS_COUNTER = False
|
||||
|
||||
# KDF import fails for 2.0.1, requires >= 2.6.1 from pip
|
||||
try:
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
HAS_PBKDF2 = True
|
||||
except ImportError:
|
||||
HAS_PBKDF2 = False
|
||||
|
||||
# AES IMPORTS
|
||||
try:
|
||||
from Crypto.Cipher import AES as AES
|
||||
HAS_AES = True
|
||||
except ImportError:
|
||||
HAS_AES = False
|
||||
|
||||
CRYPTO_UPGRADE = "ansible-vault requires a newer version of pycrypto than the one installed on your platform. You may fix this with OS-specific commands such as: yum install python-devel; rpm -e --nodeps python-crypto; pip install pycrypto"
|
||||
|
||||
HEADER=u'$ANSIBLE_VAULT'
|
||||
CIPHER_WHITELIST=['AES', 'AES256']
|
||||
|
||||
|
||||
class VaultLib(object):
|
||||
|
||||
def __init__(self, password):
|
||||
self.password = password
|
||||
self.cipher_name = None
|
||||
self.version = '1.1'
|
||||
|
||||
def is_encrypted(self, data):
|
||||
data = to_unicode(data)
|
||||
if data.startswith(HEADER):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def encrypt(self, data):
|
||||
data = to_unicode(data)
|
||||
|
||||
if self.is_encrypted(data):
|
||||
raise errors.AnsibleError("data is already encrypted")
|
||||
|
||||
if not self.cipher_name:
|
||||
self.cipher_name = "AES256"
|
||||
# raise errors.AnsibleError("the cipher must be set before encrypting data")
|
||||
|
||||
if 'Vault' + self.cipher_name in globals() and self.cipher_name in CIPHER_WHITELIST:
|
||||
cipher = globals()['Vault' + self.cipher_name]
|
||||
this_cipher = cipher()
|
||||
else:
|
||||
raise errors.AnsibleError("{0} cipher could not be found".format(self.cipher_name))
|
||||
|
||||
"""
|
||||
# combine sha + data
|
||||
this_sha = sha256(data).hexdigest()
|
||||
tmp_data = this_sha + "\n" + data
|
||||
"""
|
||||
|
||||
# encrypt sha + data
|
||||
enc_data = this_cipher.encrypt(data, self.password)
|
||||
|
||||
# add header
|
||||
tmp_data = self._add_header(enc_data)
|
||||
return tmp_data
|
||||
|
||||
def decrypt(self, data):
|
||||
data = to_bytes(data)
|
||||
|
||||
if self.password is None:
|
||||
raise errors.AnsibleError("A vault password must be specified to decrypt data")
|
||||
|
||||
if not self.is_encrypted(data):
|
||||
raise errors.AnsibleError("data is not encrypted")
|
||||
|
||||
# clean out header
|
||||
data = self._split_header(data)
|
||||
|
||||
# create the cipher object
|
||||
ciphername = to_unicode(self.cipher_name)
|
||||
if 'Vault' + ciphername in globals() and ciphername in CIPHER_WHITELIST:
|
||||
cipher = globals()['Vault' + ciphername]
|
||||
this_cipher = cipher()
|
||||
else:
|
||||
raise errors.AnsibleError("{0} cipher could not be found".format(ciphername))
|
||||
|
||||
# try to unencrypt data
|
||||
data = this_cipher.decrypt(data, self.password)
|
||||
if data is None:
|
||||
raise errors.AnsibleError("Decryption failed")
|
||||
|
||||
return data
|
||||
|
||||
def _add_header(self, data):
|
||||
# combine header and encrypted data in 80 char columns
|
||||
|
||||
#tmpdata = hexlify(data)
|
||||
tmpdata = [to_bytes(data[i:i+80]) for i in range(0, len(data), 80)]
|
||||
if not self.cipher_name:
|
||||
raise errors.AnsibleError("the cipher must be set before adding a header")
|
||||
|
||||
dirty_data = to_bytes(HEADER + ";" + self.version + ";" + self.cipher_name + "\n")
|
||||
for l in tmpdata:
|
||||
dirty_data += l + b'\n'
|
||||
|
||||
return dirty_data
|
||||
|
||||
|
||||
def _split_header(self, data):
|
||||
# used by decrypt
|
||||
|
||||
tmpdata = data.split(b'\n')
|
||||
tmpheader = tmpdata[0].strip().split(b';')
|
||||
|
||||
self.version = to_unicode(tmpheader[1].strip())
|
||||
self.cipher_name = to_unicode(tmpheader[2].strip())
|
||||
clean_data = b'\n'.join(tmpdata[1:])
|
||||
|
||||
"""
|
||||
# strip out newline, join, unhex
|
||||
clean_data = [ x.strip() for x in clean_data ]
|
||||
clean_data = unhexlify(''.join(clean_data))
|
||||
"""
|
||||
|
||||
return clean_data
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *err):
|
||||
pass
|
||||
|
||||
class VaultEditor(object):
|
||||
# uses helper methods for write_file(self, filename, data)
|
||||
# to write a file so that code isn't duplicated for simple
|
||||
# file I/O, ditto read_file(self, filename) and launch_editor(self, filename)
|
||||
# ... "Don't Repeat Yourself", etc.
|
||||
|
||||
def __init__(self, cipher_name, password, filename):
|
||||
# instantiates a member variable for VaultLib
|
||||
self.cipher_name = cipher_name
|
||||
self.password = password
|
||||
self.filename = filename
|
||||
|
||||
def _edit_file_helper(self, existing_data=None, cipher=None):
|
||||
# make sure the umask is set to a sane value
|
||||
old_umask = os.umask(0o077)
|
||||
|
||||
# Create a tempfile
|
||||
_, tmp_path = tempfile.mkstemp()
|
||||
|
||||
if existing_data:
|
||||
self.write_data(existing_data, tmp_path)
|
||||
|
||||
# drop the user into an editor on the tmp file
|
||||
call(self._editor_shell_command(tmp_path))
|
||||
tmpdata = self.read_data(tmp_path)
|
||||
|
||||
# create new vault
|
||||
this_vault = VaultLib(self.password)
|
||||
if cipher:
|
||||
this_vault.cipher_name = cipher
|
||||
|
||||
# encrypt new data and write out to tmp
|
||||
enc_data = this_vault.encrypt(tmpdata)
|
||||
self.write_data(enc_data, tmp_path)
|
||||
|
||||
# shuffle tmp file into place
|
||||
self.shuffle_files(tmp_path, self.filename)
|
||||
|
||||
# and restore umask
|
||||
os.umask(old_umask)
|
||||
|
||||
def create_file(self):
|
||||
""" create a new encrypted file """
|
||||
|
||||
if not HAS_AES or not HAS_COUNTER or not HAS_PBKDF2 or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
if os.path.isfile(self.filename):
|
||||
raise errors.AnsibleError("%s exists, please use 'edit' instead" % self.filename)
|
||||
|
||||
# Let the user specify contents and save file
|
||||
self._edit_file_helper(cipher=self.cipher_name)
|
||||
|
||||
def decrypt_file(self):
|
||||
|
||||
if not HAS_AES or not HAS_COUNTER or not HAS_PBKDF2 or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
if not os.path.isfile(self.filename):
|
||||
raise errors.AnsibleError("%s does not exist" % self.filename)
|
||||
|
||||
tmpdata = self.read_data(self.filename)
|
||||
this_vault = VaultLib(self.password)
|
||||
if this_vault.is_encrypted(tmpdata):
|
||||
dec_data = this_vault.decrypt(tmpdata)
|
||||
if dec_data is None:
|
||||
raise errors.AnsibleError("Decryption failed")
|
||||
else:
|
||||
self.write_data(dec_data, self.filename)
|
||||
else:
|
||||
raise errors.AnsibleError("%s is not encrypted" % self.filename)
|
||||
|
||||
def edit_file(self):
|
||||
|
||||
if not HAS_AES or not HAS_COUNTER or not HAS_PBKDF2 or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
# decrypt to tmpfile
|
||||
tmpdata = self.read_data(self.filename)
|
||||
this_vault = VaultLib(self.password)
|
||||
dec_data = this_vault.decrypt(tmpdata)
|
||||
|
||||
# let the user edit the data and save
|
||||
self._edit_file_helper(existing_data=dec_data)
|
||||
###we want the cipher to default to AES256 (get rid of files
|
||||
# encrypted with the AES cipher)
|
||||
#self._edit_file_helper(existing_data=dec_data, cipher=this_vault.cipher_name)
|
||||
|
||||
|
||||
def view_file(self):
|
||||
|
||||
if not HAS_AES or not HAS_COUNTER or not HAS_PBKDF2 or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
# decrypt to tmpfile
|
||||
tmpdata = self.read_data(self.filename)
|
||||
this_vault = VaultLib(self.password)
|
||||
dec_data = this_vault.decrypt(tmpdata)
|
||||
_, tmp_path = tempfile.mkstemp()
|
||||
self.write_data(dec_data, tmp_path)
|
||||
|
||||
# drop the user into pager on the tmp file
|
||||
call(self._pager_shell_command(tmp_path))
|
||||
os.remove(tmp_path)
|
||||
|
||||
def encrypt_file(self):
|
||||
|
||||
if not HAS_AES or not HAS_COUNTER or not HAS_PBKDF2 or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
if not os.path.isfile(self.filename):
|
||||
raise errors.AnsibleError("%s does not exist" % self.filename)
|
||||
|
||||
tmpdata = self.read_data(self.filename)
|
||||
this_vault = VaultLib(self.password)
|
||||
this_vault.cipher_name = self.cipher_name
|
||||
if not this_vault.is_encrypted(tmpdata):
|
||||
enc_data = this_vault.encrypt(tmpdata)
|
||||
self.write_data(enc_data, self.filename)
|
||||
else:
|
||||
raise errors.AnsibleError("%s is already encrypted" % self.filename)
|
||||
|
||||
def rekey_file(self, new_password):
|
||||
|
||||
if not HAS_AES or not HAS_COUNTER or not HAS_PBKDF2 or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
# decrypt
|
||||
tmpdata = self.read_data(self.filename)
|
||||
this_vault = VaultLib(self.password)
|
||||
dec_data = this_vault.decrypt(tmpdata)
|
||||
|
||||
# create new vault
|
||||
new_vault = VaultLib(new_password)
|
||||
|
||||
# we want to force cipher to the default
|
||||
#new_vault.cipher_name = this_vault.cipher_name
|
||||
|
||||
# re-encrypt data and re-write file
|
||||
enc_data = new_vault.encrypt(dec_data)
|
||||
self.write_data(enc_data, self.filename)
|
||||
|
||||
def read_data(self, filename):
|
||||
f = open(filename, "rb")
|
||||
tmpdata = f.read()
|
||||
f.close()
|
||||
return tmpdata
|
||||
|
||||
def write_data(self, data, filename):
|
||||
if os.path.isfile(filename):
|
||||
os.remove(filename)
|
||||
f = open(filename, "wb")
|
||||
f.write(to_bytes(data))
|
||||
f.close()
|
||||
|
||||
def shuffle_files(self, src, dest):
|
||||
# overwrite dest with src
|
||||
if os.path.isfile(dest):
|
||||
os.remove(dest)
|
||||
shutil.move(src, dest)
|
||||
|
||||
def _editor_shell_command(self, filename):
|
||||
EDITOR = os.environ.get('EDITOR','vim')
|
||||
editor = shlex.split(EDITOR)
|
||||
editor.append(filename)
|
||||
|
||||
return editor
|
||||
|
||||
def _pager_shell_command(self, filename):
|
||||
PAGER = os.environ.get('PAGER','less')
|
||||
pager = shlex.split(PAGER)
|
||||
pager.append(filename)
|
||||
|
||||
return pager
|
||||
|
||||
########################################
|
||||
# CIPHERS #
|
||||
########################################
|
||||
|
||||
class VaultAES(object):
|
||||
|
||||
# this version has been obsoleted by the VaultAES256 class
|
||||
# which uses encrypt-then-mac (fixing order) and also improving the KDF used
|
||||
# code remains for upgrade purposes only
|
||||
# http://stackoverflow.com/a/16761459
|
||||
|
||||
def __init__(self):
|
||||
if not HAS_AES:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
def aes_derive_key_and_iv(self, password, salt, key_length, iv_length):
|
||||
|
||||
""" Create a key and an initialization vector """
|
||||
|
||||
d = d_i = b''
|
||||
while len(d) < key_length + iv_length:
|
||||
text = "{0}{1}{2}".format(d_i, password, salt)
|
||||
d_i = md5(to_bytes(text)).digest()
|
||||
d += d_i
|
||||
|
||||
key = d[:key_length]
|
||||
iv = d[key_length:key_length+iv_length]
|
||||
|
||||
return key, iv
|
||||
|
||||
def encrypt(self, data, password, key_length=32):
|
||||
|
||||
""" Read plaintext data from in_file and write encrypted to out_file """
|
||||
|
||||
|
||||
# combine sha + data
|
||||
this_sha = sha256(to_bytes(data)).hexdigest()
|
||||
tmp_data = this_sha + "\n" + data
|
||||
|
||||
in_file = BytesIO(to_bytes(tmp_data))
|
||||
in_file.seek(0)
|
||||
out_file = BytesIO()
|
||||
|
||||
bs = AES.block_size
|
||||
|
||||
# Get a block of random data. EL does not have Crypto.Random.new()
|
||||
# so os.urandom is used for cross platform purposes
|
||||
salt = os.urandom(bs - len('Salted__'))
|
||||
|
||||
key, iv = self.aes_derive_key_and_iv(password, salt, key_length, bs)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
full = to_bytes(b'Salted__' + salt)
|
||||
out_file.write(full)
|
||||
finished = False
|
||||
while not finished:
|
||||
chunk = in_file.read(1024 * bs)
|
||||
if len(chunk) == 0 or len(chunk) % bs != 0:
|
||||
padding_length = (bs - len(chunk) % bs) or bs
|
||||
chunk += to_bytes(padding_length * chr(padding_length))
|
||||
finished = True
|
||||
out_file.write(cipher.encrypt(chunk))
|
||||
|
||||
out_file.seek(0)
|
||||
enc_data = out_file.read()
|
||||
tmp_data = hexlify(enc_data)
|
||||
|
||||
return tmp_data
|
||||
|
||||
|
||||
def decrypt(self, data, password, key_length=32):
|
||||
|
||||
""" Read encrypted data from in_file and write decrypted to out_file """
|
||||
|
||||
# http://stackoverflow.com/a/14989032
|
||||
|
||||
data = b''.join(data.split(b'\n'))
|
||||
data = unhexlify(data)
|
||||
|
||||
in_file = BytesIO(data)
|
||||
in_file.seek(0)
|
||||
out_file = BytesIO()
|
||||
|
||||
bs = AES.block_size
|
||||
tmpsalt = in_file.read(bs)
|
||||
salt = tmpsalt[len('Salted__'):]
|
||||
key, iv = self.aes_derive_key_and_iv(password, salt, key_length, bs)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
next_chunk = b''
|
||||
finished = False
|
||||
|
||||
while not finished:
|
||||
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
|
||||
if len(next_chunk) == 0:
|
||||
if PY2:
|
||||
padding_length = ord(chunk[-1])
|
||||
else:
|
||||
padding_length = chunk[-1]
|
||||
|
||||
chunk = chunk[:-padding_length]
|
||||
finished = True
|
||||
|
||||
out_file.write(chunk)
|
||||
out_file.flush()
|
||||
|
||||
# reset the stream pointer to the beginning
|
||||
out_file.seek(0)
|
||||
out_data = out_file.read()
|
||||
out_file.close()
|
||||
new_data = to_unicode(out_data)
|
||||
|
||||
# split out sha and verify decryption
|
||||
split_data = new_data.split("\n")
|
||||
this_sha = split_data[0]
|
||||
this_data = '\n'.join(split_data[1:])
|
||||
test_sha = sha256(to_bytes(this_data)).hexdigest()
|
||||
|
||||
if this_sha != test_sha:
|
||||
raise errors.AnsibleError("Decryption failed")
|
||||
|
||||
return this_data
|
||||
|
||||
|
||||
class VaultAES256(object):
|
||||
|
||||
"""
|
||||
Vault implementation using AES-CTR with an HMAC-SHA256 authentication code.
|
||||
Keys are derived using PBKDF2
|
||||
"""
|
||||
|
||||
# http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
|
||||
|
||||
def __init__(self):
|
||||
|
||||
if not HAS_PBKDF2 or not HAS_COUNTER or not HAS_HASH:
|
||||
raise errors.AnsibleError(CRYPTO_UPGRADE)
|
||||
|
||||
def gen_key_initctr(self, password, salt):
|
||||
# 16 for AES 128, 32 for AES256
|
||||
keylength = 32
|
||||
|
||||
# match the size used for counter.new to avoid extra work
|
||||
ivlength = 16
|
||||
|
||||
hash_function = SHA256
|
||||
|
||||
# make two keys and one iv
|
||||
pbkdf2_prf = lambda p, s: HMAC.new(p, s, hash_function).digest()
|
||||
|
||||
|
||||
derivedkey = PBKDF2(password, salt, dkLen=(2 * keylength) + ivlength,
|
||||
count=10000, prf=pbkdf2_prf)
|
||||
|
||||
key1 = derivedkey[:keylength]
|
||||
key2 = derivedkey[keylength:(keylength * 2)]
|
||||
iv = derivedkey[(keylength * 2):(keylength * 2) + ivlength]
|
||||
|
||||
return key1, key2, hexlify(iv)
|
||||
|
||||
|
||||
def encrypt(self, data, password):
|
||||
|
||||
salt = os.urandom(32)
|
||||
key1, key2, iv = self.gen_key_initctr(password, salt)
|
||||
|
||||
# PKCS#7 PAD DATA http://tools.ietf.org/html/rfc5652#section-6.3
|
||||
bs = AES.block_size
|
||||
padding_length = (bs - len(data) % bs) or bs
|
||||
data += padding_length * chr(padding_length)
|
||||
|
||||
# COUNTER.new PARAMETERS
|
||||
# 1) nbits (integer) - Length of the counter, in bits.
|
||||
# 2) initial_value (integer) - initial value of the counter. "iv" from gen_key_initctr
|
||||
|
||||
ctr = Counter.new(128, initial_value=int(iv, 16))
|
||||
|
||||
# AES.new PARAMETERS
|
||||
# 1) AES key, must be either 16, 24, or 32 bytes long -- "key" from gen_key_initctr
|
||||
# 2) MODE_CTR, is the recommended mode
|
||||
# 3) counter=<CounterObject>
|
||||
|
||||
cipher = AES.new(key1, AES.MODE_CTR, counter=ctr)
|
||||
|
||||
# ENCRYPT PADDED DATA
|
||||
cryptedData = cipher.encrypt(data)
|
||||
|
||||
# COMBINE SALT, DIGEST AND DATA
|
||||
hmac = HMAC.new(key2, cryptedData, SHA256)
|
||||
message = b''.join([hexlify(salt), b"\n", to_bytes(hmac.hexdigest()), b"\n", hexlify(cryptedData)])
|
||||
message = hexlify(message)
|
||||
return message
|
||||
|
||||
def decrypt(self, data, password):
|
||||
|
||||
# SPLIT SALT, DIGEST, AND DATA
|
||||
data = b''.join(data.split(b"\n"))
|
||||
data = unhexlify(data)
|
||||
salt, cryptedHmac, cryptedData = data.split(b"\n", 2)
|
||||
salt = unhexlify(salt)
|
||||
cryptedData = unhexlify(cryptedData)
|
||||
|
||||
key1, key2, iv = self.gen_key_initctr(password, salt)
|
||||
|
||||
# EXIT EARLY IF DIGEST DOESN'T MATCH
|
||||
hmacDecrypt = HMAC.new(key2, cryptedData, SHA256)
|
||||
if not self.is_equal(cryptedHmac, to_bytes(hmacDecrypt.hexdigest())):
|
||||
return None
|
||||
|
||||
# SET THE COUNTER AND THE CIPHER
|
||||
ctr = Counter.new(128, initial_value=int(iv, 16))
|
||||
cipher = AES.new(key1, AES.MODE_CTR, counter=ctr)
|
||||
|
||||
# DECRYPT PADDED DATA
|
||||
decryptedData = cipher.decrypt(cryptedData)
|
||||
|
||||
# UNPAD DATA
|
||||
try:
|
||||
padding_length = ord(decryptedData[-1])
|
||||
except TypeError:
|
||||
padding_length = decryptedData[-1]
|
||||
|
||||
decryptedData = decryptedData[:-padding_length]
|
||||
|
||||
return to_unicode(decryptedData)
|
||||
|
||||
def is_equal(self, a, b):
|
||||
"""
|
||||
Comparing 2 byte arrrays in constant time
|
||||
to avoid timing attacks.
|
||||
|
||||
It would be nice if there was a library for this but
|
||||
hey.
|
||||
"""
|
||||
# http://codahale.com/a-lesson-in-timing-attacks/
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
|
||||
result = 0
|
||||
for x, y in zip(a, b):
|
||||
if PY2:
|
||||
result |= ord(x) ^ ord(y)
|
||||
else:
|
||||
result |= x ^ y
|
||||
return result == 0
|
||||
21
lib/ansible/parsing/yaml/__init__.py
Normal file
21
lib/ansible/parsing/yaml/__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
|
||||
|
||||
91
lib/ansible/parsing/yaml/constructor.py
Normal file
91
lib/ansible/parsing/yaml/constructor.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# (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 yaml.constructor import Constructor
|
||||
from ansible.parsing.yaml.objects import AnsibleMapping, AnsibleSequence, AnsibleUnicode
|
||||
|
||||
class AnsibleConstructor(Constructor):
|
||||
def __init__(self, file_name=None):
|
||||
self._ansible_file_name = file_name
|
||||
super(AnsibleConstructor, self).__init__()
|
||||
|
||||
def construct_yaml_map(self, node):
|
||||
data = AnsibleMapping()
|
||||
yield data
|
||||
value = self.construct_mapping(node)
|
||||
data.update(value)
|
||||
data.ansible_pos = self._node_position_info(node)
|
||||
|
||||
def construct_mapping(self, node, deep=False):
|
||||
ret = AnsibleMapping(super(Constructor, self).construct_mapping(node, deep))
|
||||
ret.ansible_pos = self._node_position_info(node)
|
||||
|
||||
return ret
|
||||
|
||||
def construct_yaml_str(self, node):
|
||||
# Override the default string handling function
|
||||
# to always return unicode objects
|
||||
value = self.construct_scalar(node)
|
||||
ret = AnsibleUnicode(value)
|
||||
|
||||
ret.ansible_pos = self._node_position_info(node)
|
||||
|
||||
return ret
|
||||
|
||||
def construct_yaml_seq(self, node):
|
||||
data = AnsibleSequence()
|
||||
yield data
|
||||
data.extend(self.construct_sequence(node))
|
||||
data.ansible_pos = self._node_position_info(node)
|
||||
|
||||
def _node_position_info(self, node):
|
||||
# the line number where the previous token has ended (plus empty lines)
|
||||
# Add one so that the first line is line 1 rather than line 0
|
||||
column = node.start_mark.column + 1
|
||||
line = node.start_mark.line + 1
|
||||
|
||||
# in some cases, we may have pre-read the data and then
|
||||
# passed it to the load() call for YAML, in which case we
|
||||
# want to override the default datasource (which would be
|
||||
# '<string>') to the actual filename we read in
|
||||
datasource = self._ansible_file_name or node.start_mark.name
|
||||
|
||||
return (datasource, line, column)
|
||||
|
||||
AnsibleConstructor.add_constructor(
|
||||
u'tag:yaml.org,2002:map',
|
||||
AnsibleConstructor.construct_yaml_map)
|
||||
|
||||
AnsibleConstructor.add_constructor(
|
||||
u'tag:yaml.org,2002:python/dict',
|
||||
AnsibleConstructor.construct_yaml_map)
|
||||
|
||||
AnsibleConstructor.add_constructor(
|
||||
u'tag:yaml.org,2002:str',
|
||||
AnsibleConstructor.construct_yaml_str)
|
||||
|
||||
AnsibleConstructor.add_constructor(
|
||||
u'tag:yaml.org,2002:python/unicode',
|
||||
AnsibleConstructor.construct_yaml_str)
|
||||
|
||||
AnsibleConstructor.add_constructor(
|
||||
u'tag:yaml.org,2002:seq',
|
||||
AnsibleConstructor.construct_yaml_seq)
|
||||
51
lib/ansible/parsing/yaml/loader.py
Normal file
51
lib/ansible/parsing/yaml/loader.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# (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
|
||||
|
||||
try:
|
||||
from _yaml import CParser, CEmitter
|
||||
HAVE_PYYAML_C = True
|
||||
except ImportError:
|
||||
HAVE_PYYAML_C = False
|
||||
|
||||
from yaml.resolver import Resolver
|
||||
|
||||
from ansible.parsing.yaml.constructor import AnsibleConstructor
|
||||
|
||||
if HAVE_PYYAML_C:
|
||||
class AnsibleLoader(CParser, AnsibleConstructor, Resolver):
|
||||
def __init__(self, stream, file_name=None):
|
||||
CParser.__init__(self, stream)
|
||||
AnsibleConstructor.__init__(self, file_name=file_name)
|
||||
Resolver.__init__(self)
|
||||
else:
|
||||
from yaml.composer import Composer
|
||||
from yaml.reader import Reader
|
||||
from yaml.scanner import Scanner
|
||||
from yaml.parser import Parser
|
||||
|
||||
class AnsibleLoader(Reader, Scanner, Parser, Composer, AnsibleConstructor, Resolver):
|
||||
def __init__(self, stream, file_name=None):
|
||||
Reader.__init__(self, stream)
|
||||
Scanner.__init__(self)
|
||||
Parser.__init__(self)
|
||||
Composer.__init__(self)
|
||||
AnsibleConstructor.__init__(self, file_name=file_name)
|
||||
Resolver.__init__(self)
|
||||
65
lib/ansible/parsing/yaml/objects.py
Normal file
65
lib/ansible/parsing/yaml/objects.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# (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 import text_type
|
||||
|
||||
|
||||
class AnsibleBaseYAMLObject(object):
|
||||
'''
|
||||
the base class used to sub-class python built-in objects
|
||||
so that we can add attributes to them during yaml parsing
|
||||
|
||||
'''
|
||||
_data_source = None
|
||||
_line_number = 0
|
||||
_column_number = 0
|
||||
|
||||
def _get_ansible_position(self):
|
||||
return (self._data_source, self._line_number, self._column_number)
|
||||
|
||||
def _set_ansible_position(self, obj):
|
||||
try:
|
||||
(src, line, col) = obj
|
||||
except (TypeError, ValueError):
|
||||
raise AssertionError(
|
||||
'ansible_pos can only be set with a tuple/list '
|
||||
'of three values: source, line number, column number'
|
||||
)
|
||||
self._data_source = src
|
||||
self._line_number = line
|
||||
self._column_number = col
|
||||
|
||||
ansible_pos = property(_get_ansible_position, _set_ansible_position)
|
||||
|
||||
|
||||
class AnsibleMapping(AnsibleBaseYAMLObject, dict):
|
||||
''' sub class for dictionaries '''
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleUnicode(AnsibleBaseYAMLObject, text_type):
|
||||
''' sub class for unicode objects '''
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleSequence(AnsibleBaseYAMLObject, list):
|
||||
''' sub class for lists '''
|
||||
pass
|
||||
@@ -15,860 +15,71 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ansible.inventory
|
||||
import ansible.constants as C
|
||||
import ansible.runner
|
||||
from ansible.utils.template import template
|
||||
from ansible import utils
|
||||
from ansible import errors
|
||||
from ansible.module_utils.splitter import split_args, unquote
|
||||
import ansible.callbacks
|
||||
import ansible.cache
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import collections
|
||||
from play import Play
|
||||
import StringIO
|
||||
import pipes
|
||||
|
||||
# the setup cache stores all variables about a host
|
||||
# gathered during the setup step, while the vars cache
|
||||
# holds all other variables about a host
|
||||
SETUP_CACHE = ansible.cache.FactCache()
|
||||
VARS_CACHE = collections.defaultdict(dict)
|
||||
RESERVED_TAGS = ['all','tagged','untagged','always']
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.parsing import DataLoader
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.playbook.play import Play
|
||||
from ansible.playbook.playbook_include import PlaybookInclude
|
||||
from ansible.plugins import push_basedir
|
||||
|
||||
|
||||
class PlayBook(object):
|
||||
'''
|
||||
runs an ansible playbook, given as a datastructure or YAML filename.
|
||||
A playbook is a deployment, config management, or automation based
|
||||
set of commands to run in series.
|
||||
__all__ = ['Playbook']
|
||||
|
||||
multiple plays/tasks do not execute simultaneously, but tasks in each
|
||||
pattern do execute in parallel (according to the number of forks
|
||||
requested) among the hosts they address
|
||||
'''
|
||||
|
||||
# *****************************************************
|
||||
class Playbook:
|
||||
|
||||
def __init__(self,
|
||||
playbook = None,
|
||||
host_list = C.DEFAULT_HOST_LIST,
|
||||
module_path = None,
|
||||
forks = C.DEFAULT_FORKS,
|
||||
timeout = C.DEFAULT_TIMEOUT,
|
||||
remote_user = C.DEFAULT_REMOTE_USER,
|
||||
remote_pass = C.DEFAULT_REMOTE_PASS,
|
||||
remote_port = None,
|
||||
transport = C.DEFAULT_TRANSPORT,
|
||||
private_key_file = C.DEFAULT_PRIVATE_KEY_FILE,
|
||||
callbacks = None,
|
||||
runner_callbacks = None,
|
||||
stats = None,
|
||||
extra_vars = None,
|
||||
only_tags = None,
|
||||
skip_tags = None,
|
||||
subset = C.DEFAULT_SUBSET,
|
||||
inventory = None,
|
||||
check = False,
|
||||
diff = False,
|
||||
any_errors_fatal = False,
|
||||
vault_password = False,
|
||||
force_handlers = False,
|
||||
# privilege escalation
|
||||
become = C.DEFAULT_BECOME,
|
||||
become_method = C.DEFAULT_BECOME_METHOD,
|
||||
become_user = C.DEFAULT_BECOME_USER,
|
||||
become_pass = None,
|
||||
):
|
||||
def __init__(self, loader):
|
||||
# Entries in the datastructure of a playbook may
|
||||
# be either a play or an include statement
|
||||
self._entries = []
|
||||
self._basedir = os.getcwd()
|
||||
self._loader = loader
|
||||
|
||||
"""
|
||||
playbook: path to a playbook file
|
||||
host_list: path to a file like /etc/ansible/hosts
|
||||
module_path: path to ansible modules, like /usr/share/ansible/
|
||||
forks: desired level of parallelism
|
||||
timeout: connection timeout
|
||||
remote_user: run as this user if not specified in a particular play
|
||||
remote_pass: use this remote password (for all plays) vs using SSH keys
|
||||
remote_port: default remote port to use if not specified with the host or play
|
||||
transport: how to connect to hosts that don't specify a transport (local, paramiko, etc)
|
||||
callbacks output callbacks for the playbook
|
||||
runner_callbacks: more callbacks, this time for the runner API
|
||||
stats: holds aggregrate data about events occurring to each host
|
||||
inventory: can be specified instead of host_list to use a pre-existing inventory object
|
||||
check: don't change anything, just try to detect some potential changes
|
||||
any_errors_fatal: terminate the entire execution immediately when one of the hosts has failed
|
||||
force_handlers: continue to notify and run handlers even if a task fails
|
||||
"""
|
||||
@staticmethod
|
||||
def load(file_name, variable_manager=None, loader=None):
|
||||
pb = Playbook(loader=loader)
|
||||
pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager)
|
||||
return pb
|
||||
|
||||
self.SETUP_CACHE = SETUP_CACHE
|
||||
self.VARS_CACHE = VARS_CACHE
|
||||
def _load_playbook_data(self, file_name, variable_manager):
|
||||
|
||||
arguments = []
|
||||
if playbook is None:
|
||||
arguments.append('playbook')
|
||||
if callbacks is None:
|
||||
arguments.append('callbacks')
|
||||
if runner_callbacks is None:
|
||||
arguments.append('runner_callbacks')
|
||||
if stats is None:
|
||||
arguments.append('stats')
|
||||
if arguments:
|
||||
raise Exception('PlayBook missing required arguments: %s' % ', '.join(arguments))
|
||||
|
||||
if extra_vars is None:
|
||||
extra_vars = {}
|
||||
if only_tags is None:
|
||||
only_tags = [ 'all' ]
|
||||
if skip_tags is None:
|
||||
skip_tags = []
|
||||
|
||||
self.check = check
|
||||
self.diff = diff
|
||||
self.module_path = module_path
|
||||
self.forks = forks
|
||||
self.timeout = timeout
|
||||
self.remote_user = remote_user
|
||||
self.remote_pass = remote_pass
|
||||
self.remote_port = remote_port
|
||||
self.transport = transport
|
||||
self.callbacks = callbacks
|
||||
self.runner_callbacks = runner_callbacks
|
||||
self.stats = stats
|
||||
self.extra_vars = extra_vars
|
||||
self.global_vars = {}
|
||||
self.private_key_file = private_key_file
|
||||
self.only_tags = only_tags
|
||||
self.skip_tags = skip_tags
|
||||
self.any_errors_fatal = any_errors_fatal
|
||||
self.vault_password = vault_password
|
||||
self.force_handlers = force_handlers
|
||||
|
||||
self.become = become
|
||||
self.become_method = become_method
|
||||
self.become_user = become_user
|
||||
self.become_pass = become_pass
|
||||
|
||||
self.callbacks.playbook = self
|
||||
self.runner_callbacks.playbook = self
|
||||
|
||||
if inventory is None:
|
||||
self.inventory = ansible.inventory.Inventory(host_list)
|
||||
self.inventory.subset(subset)
|
||||
if os.path.isabs(file_name):
|
||||
self._basedir = os.path.dirname(file_name)
|
||||
else:
|
||||
self.inventory = inventory
|
||||
self._basedir = os.path.normpath(os.path.join(self._basedir, os.path.dirname(file_name)))
|
||||
|
||||
if self.module_path is not None:
|
||||
utils.plugins.module_finder.add_directory(self.module_path)
|
||||
# set the loaders basedir
|
||||
self._loader.set_basedir(self._basedir)
|
||||
|
||||
self.basedir = os.path.dirname(playbook) or '.'
|
||||
utils.plugins.push_basedir(self.basedir)
|
||||
# also add the basedir to the list of module directories
|
||||
push_basedir(self._basedir)
|
||||
|
||||
# let inventory know the playbook basedir so it can load more vars
|
||||
self.inventory.set_playbook_basedir(self.basedir)
|
||||
ds = self._loader.load_from_file(os.path.basename(file_name))
|
||||
if not isinstance(ds, list):
|
||||
raise AnsibleParserError("playbooks must be a list of plays", obj=ds)
|
||||
|
||||
vars = extra_vars.copy()
|
||||
vars['playbook_dir'] = os.path.abspath(self.basedir)
|
||||
if self.inventory.basedir() is not None:
|
||||
vars['inventory_dir'] = self.inventory.basedir()
|
||||
|
||||
if self.inventory.src() is not None:
|
||||
vars['inventory_file'] = self.inventory.src()
|
||||
|
||||
self.filename = playbook
|
||||
(self.playbook, self.play_basedirs) = self._load_playbook_from_file(playbook, vars)
|
||||
ansible.callbacks.load_callback_plugins()
|
||||
ansible.callbacks.set_playbook(self.callbacks, self)
|
||||
|
||||
self._ansible_version = utils.version_info(gitinfo=True)
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _get_playbook_vars(self, play_ds, existing_vars):
|
||||
'''
|
||||
Gets the vars specified with the play and blends them
|
||||
with any existing vars that have already been read in
|
||||
'''
|
||||
new_vars = existing_vars.copy()
|
||||
if 'vars' in play_ds:
|
||||
if isinstance(play_ds['vars'], dict):
|
||||
new_vars.update(play_ds['vars'])
|
||||
elif isinstance(play_ds['vars'], list):
|
||||
for v in play_ds['vars']:
|
||||
new_vars.update(v)
|
||||
return new_vars
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _get_include_info(self, play_ds, basedir, existing_vars={}):
|
||||
'''
|
||||
Gets any key=value pairs specified with the included file
|
||||
name and returns the merged vars along with the path
|
||||
'''
|
||||
new_vars = existing_vars.copy()
|
||||
tokens = split_args(play_ds.get('include', ''))
|
||||
for t in tokens[1:]:
|
||||
try:
|
||||
(k,v) = unquote(t).split("=", 1)
|
||||
new_vars[k] = template(basedir, v, new_vars)
|
||||
except ValueError, e:
|
||||
raise errors.AnsibleError('included playbook variables must be in the form k=v, got: %s' % t)
|
||||
|
||||
return (new_vars, unquote(tokens[0]))
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _get_playbook_vars_files(self, play_ds, existing_vars_files):
|
||||
new_vars_files = list(existing_vars_files)
|
||||
if 'vars_files' in play_ds:
|
||||
new_vars_files = utils.list_union(new_vars_files, play_ds['vars_files'])
|
||||
return new_vars_files
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _extend_play_vars(self, play, vars={}):
|
||||
'''
|
||||
Extends the given play's variables with the additional specified vars.
|
||||
'''
|
||||
|
||||
if 'vars' not in play or not play['vars']:
|
||||
# someone left out or put an empty "vars:" entry in their playbook
|
||||
return vars.copy()
|
||||
|
||||
play_vars = None
|
||||
if isinstance(play['vars'], dict):
|
||||
play_vars = play['vars'].copy()
|
||||
play_vars.update(vars)
|
||||
elif isinstance(play['vars'], list):
|
||||
# nobody should really do this, but handle vars: a=1 b=2
|
||||
play_vars = play['vars'][:]
|
||||
play_vars.extend([{k:v} for k,v in vars.iteritems()])
|
||||
|
||||
return play_vars
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _load_playbook_from_file(self, path, vars={}, vars_files=[]):
|
||||
'''
|
||||
run top level error checking on playbooks and allow them to include other playbooks.
|
||||
'''
|
||||
|
||||
playbook_data = utils.parse_yaml_from_file(path, vault_password=self.vault_password)
|
||||
accumulated_plays = []
|
||||
play_basedirs = []
|
||||
|
||||
if type(playbook_data) != list:
|
||||
raise errors.AnsibleError("parse error: playbooks must be formatted as a YAML list, got %s" % type(playbook_data))
|
||||
|
||||
basedir = os.path.dirname(path) or '.'
|
||||
utils.plugins.push_basedir(basedir)
|
||||
for play in playbook_data:
|
||||
if type(play) != dict:
|
||||
raise errors.AnsibleError("parse error: each play in a playbook must be a YAML dictionary (hash), received: %s" % play)
|
||||
|
||||
if 'include' in play:
|
||||
# a playbook (list of plays) decided to include some other list of plays
|
||||
# from another file. The result is a flat list of plays in the end.
|
||||
|
||||
play_vars = self._get_playbook_vars(play, vars)
|
||||
play_vars_files = self._get_playbook_vars_files(play, vars_files)
|
||||
inc_vars, inc_path = self._get_include_info(play, basedir, play_vars)
|
||||
play_vars.update(inc_vars)
|
||||
|
||||
included_path = utils.path_dwim(basedir, template(basedir, inc_path, play_vars))
|
||||
(plays, basedirs) = self._load_playbook_from_file(included_path, vars=play_vars, vars_files=play_vars_files)
|
||||
for p in plays:
|
||||
# support for parameterized play includes works by passing
|
||||
# those variables along to the subservient play
|
||||
p['vars'] = self._extend_play_vars(p, play_vars)
|
||||
# now add in the vars_files
|
||||
p['vars_files'] = utils.list_union(p.get('vars_files', []), play_vars_files)
|
||||
|
||||
accumulated_plays.extend(plays)
|
||||
play_basedirs.extend(basedirs)
|
||||
# Parse the playbook entries. For plays, we simply parse them
|
||||
# using the Play() object, and includes are parsed using the
|
||||
# PlaybookInclude() object
|
||||
for entry in ds:
|
||||
if not isinstance(entry, dict):
|
||||
raise AnsibleParserError("playbook entries must be either a valid play or an include statement", obj=entry)
|
||||
|
||||
if 'include' in entry:
|
||||
pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader)
|
||||
self._entries.extend(pb._entries)
|
||||
else:
|
||||
entry_obj = Play.load(entry, variable_manager=variable_manager, loader=self._loader)
|
||||
self._entries.append(entry_obj)
|
||||
|
||||
# this is a normal (non-included play)
|
||||
accumulated_plays.append(play)
|
||||
play_basedirs.append(basedir)
|
||||
def get_loader(self):
|
||||
return self._loader
|
||||
|
||||
return (accumulated_plays, play_basedirs)
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def run(self):
|
||||
''' run all patterns in the playbook '''
|
||||
plays = []
|
||||
matched_tags_all = set()
|
||||
unmatched_tags_all = set()
|
||||
|
||||
# loop through all patterns and run them
|
||||
self.callbacks.on_start()
|
||||
for (play_ds, play_basedir) in zip(self.playbook, self.play_basedirs):
|
||||
play = Play(self, play_ds, play_basedir, vault_password=self.vault_password)
|
||||
assert play is not None
|
||||
|
||||
matched_tags, unmatched_tags = play.compare_tags(self.only_tags)
|
||||
|
||||
matched_tags_all = matched_tags_all | matched_tags
|
||||
unmatched_tags_all = unmatched_tags_all | unmatched_tags
|
||||
|
||||
# Remove tasks we wish to skip
|
||||
matched_tags = matched_tags - set(self.skip_tags)
|
||||
|
||||
# if we have matched_tags, the play must be run.
|
||||
# if the play contains no tasks, assume we just want to gather facts
|
||||
# in this case there are actually 3 meta tasks (handler flushes) not 0
|
||||
# tasks, so that's why there's a check against 3
|
||||
if (len(matched_tags) > 0 or len(play.tasks()) == 3):
|
||||
plays.append(play)
|
||||
|
||||
# if the playbook is invoked with --tags or --skip-tags that don't
|
||||
# exist at all in the playbooks then we need to raise an error so that
|
||||
# the user can correct the arguments.
|
||||
unknown_tags = ((set(self.only_tags) | set(self.skip_tags)) -
|
||||
(matched_tags_all | unmatched_tags_all))
|
||||
|
||||
for t in RESERVED_TAGS:
|
||||
unknown_tags.discard(t)
|
||||
|
||||
if len(unknown_tags) > 0:
|
||||
for t in RESERVED_TAGS:
|
||||
unmatched_tags_all.discard(t)
|
||||
msg = 'tag(s) not found in playbook: %s. possible values: %s'
|
||||
unknown = ','.join(sorted(unknown_tags))
|
||||
unmatched = ','.join(sorted(unmatched_tags_all))
|
||||
raise errors.AnsibleError(msg % (unknown, unmatched))
|
||||
|
||||
for play in plays:
|
||||
ansible.callbacks.set_play(self.callbacks, play)
|
||||
ansible.callbacks.set_play(self.runner_callbacks, play)
|
||||
if not self._run_play(play):
|
||||
break
|
||||
|
||||
ansible.callbacks.set_play(self.callbacks, None)
|
||||
ansible.callbacks.set_play(self.runner_callbacks, None)
|
||||
|
||||
# summarize the results
|
||||
results = {}
|
||||
for host in self.stats.processed.keys():
|
||||
results[host] = self.stats.summarize(host)
|
||||
return results
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _async_poll(self, poller, async_seconds, async_poll_interval):
|
||||
''' launch an async job, if poll_interval is set, wait for completion '''
|
||||
|
||||
results = poller.wait(async_seconds, async_poll_interval)
|
||||
|
||||
# mark any hosts that are still listed as started as failed
|
||||
# since these likely got killed by async_wrapper
|
||||
for host in poller.hosts_to_poll:
|
||||
reason = { 'failed' : 1, 'rc' : None, 'msg' : 'timed out' }
|
||||
self.runner_callbacks.on_async_failed(host, reason, poller.runner.vars_cache[host]['ansible_job_id'])
|
||||
results['contacted'][host] = reason
|
||||
|
||||
return results
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _trim_unavailable_hosts(self, hostlist=[], keep_failed=False):
|
||||
''' returns a list of hosts that haven't failed and aren't dark '''
|
||||
|
||||
return [ h for h in hostlist if (keep_failed or h not in self.stats.failures) and (h not in self.stats.dark)]
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _run_task_internal(self, task, include_failed=False):
|
||||
''' run a particular module step in a playbook '''
|
||||
|
||||
hosts = self._trim_unavailable_hosts(self.inventory.list_hosts(task.play._play_hosts), keep_failed=include_failed)
|
||||
self.inventory.restrict_to(hosts)
|
||||
|
||||
runner = ansible.runner.Runner(
|
||||
pattern=task.play.hosts,
|
||||
inventory=self.inventory,
|
||||
module_name=task.module_name,
|
||||
module_args=task.module_args,
|
||||
forks=self.forks,
|
||||
remote_pass=self.remote_pass,
|
||||
module_path=self.module_path,
|
||||
timeout=self.timeout,
|
||||
remote_user=task.remote_user,
|
||||
remote_port=task.play.remote_port,
|
||||
module_vars=task.module_vars,
|
||||
play_vars=task.play_vars,
|
||||
play_file_vars=task.play_file_vars,
|
||||
role_vars=task.role_vars,
|
||||
role_params=task.role_params,
|
||||
default_vars=task.default_vars,
|
||||
extra_vars=self.extra_vars,
|
||||
private_key_file=self.private_key_file,
|
||||
setup_cache=self.SETUP_CACHE,
|
||||
vars_cache=self.VARS_CACHE,
|
||||
basedir=task.play.basedir,
|
||||
conditional=task.when,
|
||||
callbacks=self.runner_callbacks,
|
||||
transport=task.transport,
|
||||
is_playbook=True,
|
||||
check=self.check,
|
||||
diff=self.diff,
|
||||
environment=task.environment,
|
||||
complex_args=task.args,
|
||||
accelerate=task.play.accelerate,
|
||||
accelerate_port=task.play.accelerate_port,
|
||||
accelerate_ipv6=task.play.accelerate_ipv6,
|
||||
error_on_undefined_vars=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR,
|
||||
vault_pass = self.vault_password,
|
||||
run_hosts=hosts,
|
||||
no_log=task.no_log,
|
||||
run_once=task.run_once,
|
||||
become=task.become,
|
||||
become_method=task.become_method,
|
||||
become_user=task.become_user,
|
||||
become_pass=task.become_pass,
|
||||
)
|
||||
|
||||
runner.module_vars.update({'play_hosts': hosts})
|
||||
runner.module_vars.update({'ansible_version': self._ansible_version})
|
||||
|
||||
if task.async_seconds == 0:
|
||||
results = runner.run()
|
||||
else:
|
||||
results, poller = runner.run_async(task.async_seconds)
|
||||
self.stats.compute(results)
|
||||
if task.async_poll_interval > 0:
|
||||
# if not polling, playbook requested fire and forget, so don't poll
|
||||
results = self._async_poll(poller, task.async_seconds, task.async_poll_interval)
|
||||
else:
|
||||
for (host, res) in results.get('contacted', {}).iteritems():
|
||||
self.runner_callbacks.on_async_ok(host, res, poller.runner.vars_cache[host]['ansible_job_id'])
|
||||
|
||||
contacted = results.get('contacted',{})
|
||||
dark = results.get('dark', {})
|
||||
|
||||
self.inventory.lift_restriction()
|
||||
|
||||
if len(contacted.keys()) == 0 and len(dark.keys()) == 0:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _run_task(self, play, task, is_handler):
|
||||
''' run a single task in the playbook and recursively run any subtasks. '''
|
||||
|
||||
ansible.callbacks.set_task(self.callbacks, task)
|
||||
ansible.callbacks.set_task(self.runner_callbacks, task)
|
||||
|
||||
if task.role_name:
|
||||
name = '%s | %s' % (task.role_name, task.name)
|
||||
else:
|
||||
name = task.name
|
||||
|
||||
try:
|
||||
# v1 HACK: we don't have enough information to template many names
|
||||
# at this point. Rather than making this work for all cases in
|
||||
# v1, just make this degrade gracefully. Will fix in v2
|
||||
name = template(play.basedir, name, task.module_vars, lookup_fatal=False, filter_fatal=False)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.callbacks.on_task_start(name, is_handler)
|
||||
if hasattr(self.callbacks, 'skip_task') and self.callbacks.skip_task:
|
||||
ansible.callbacks.set_task(self.callbacks, None)
|
||||
ansible.callbacks.set_task(self.runner_callbacks, None)
|
||||
return True
|
||||
|
||||
# template ignore_errors
|
||||
# TODO: Is this needed here? cond is templated again in
|
||||
# check_conditional after some more manipulations.
|
||||
# TODO: we don't have enough information here to template cond either
|
||||
# (see note on templating name above)
|
||||
cond = template(play.basedir, task.ignore_errors, task.module_vars, expand_lists=False)
|
||||
task.ignore_errors = utils.check_conditional(cond, play.basedir, task.module_vars, fail_on_undefined=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR)
|
||||
|
||||
# load up an appropriate ansible runner to run the task in parallel
|
||||
include_failed = is_handler and play.force_handlers
|
||||
results = self._run_task_internal(task, include_failed=include_failed)
|
||||
|
||||
# if no hosts are matched, carry on
|
||||
hosts_remaining = True
|
||||
if results is None:
|
||||
hosts_remaining = False
|
||||
results = {}
|
||||
|
||||
contacted = results.get('contacted', {})
|
||||
self.stats.compute(results, ignore_errors=task.ignore_errors)
|
||||
|
||||
def _register_play_vars(host, result):
|
||||
# when 'register' is used, persist the result in the vars cache
|
||||
# rather than the setup cache - vars should be transient between
|
||||
# playbook executions
|
||||
if 'stdout' in result and 'stdout_lines' not in result:
|
||||
result['stdout_lines'] = result['stdout'].splitlines()
|
||||
utils.update_hash(self.VARS_CACHE, host, {task.register: result})
|
||||
|
||||
def _save_play_facts(host, facts):
|
||||
# saves play facts in SETUP_CACHE, unless the module executed was
|
||||
# set_fact, in which case we add them to the VARS_CACHE
|
||||
if task.module_name in ('set_fact', 'include_vars'):
|
||||
utils.update_hash(self.VARS_CACHE, host, facts)
|
||||
else:
|
||||
utils.update_hash(self.SETUP_CACHE, host, facts)
|
||||
|
||||
# add facts to the global setup cache
|
||||
for host, result in contacted.iteritems():
|
||||
if 'results' in result:
|
||||
# task ran with_ lookup plugin, so facts are encapsulated in
|
||||
# multiple list items in the results key
|
||||
for res in result['results']:
|
||||
if type(res) == dict:
|
||||
facts = res.get('ansible_facts', {})
|
||||
_save_play_facts(host, facts)
|
||||
else:
|
||||
# when facts are returned, persist them in the setup cache
|
||||
facts = result.get('ansible_facts', {})
|
||||
_save_play_facts(host, facts)
|
||||
|
||||
# if requested, save the result into the registered variable name
|
||||
if task.register:
|
||||
_register_play_vars(host, result)
|
||||
|
||||
# also have to register some failed, but ignored, tasks
|
||||
if task.ignore_errors and task.register:
|
||||
failed = results.get('failed', {})
|
||||
for host, result in failed.iteritems():
|
||||
_register_play_vars(host, result)
|
||||
|
||||
# flag which notify handlers need to be run
|
||||
if len(task.notify) > 0:
|
||||
for host, results in results.get('contacted',{}).iteritems():
|
||||
if results.get('changed', False):
|
||||
for handler_name in task.notify:
|
||||
self._flag_handler(play, template(play.basedir, handler_name, task.module_vars), host)
|
||||
|
||||
ansible.callbacks.set_task(self.callbacks, None)
|
||||
ansible.callbacks.set_task(self.runner_callbacks, None)
|
||||
return hosts_remaining
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _flag_handler(self, play, handler_name, host):
|
||||
'''
|
||||
if a task has any notify elements, flag handlers for run
|
||||
at end of execution cycle for hosts that have indicated
|
||||
changes have been made
|
||||
'''
|
||||
|
||||
found = False
|
||||
for x in play.handlers():
|
||||
if handler_name == template(play.basedir, x.name, x.module_vars):
|
||||
found = True
|
||||
self.callbacks.on_notify(host, x.name)
|
||||
x.notified_by.append(host)
|
||||
if not found:
|
||||
raise errors.AnsibleError("change handler (%s) is not defined" % handler_name)
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _do_setup_step(self, play):
|
||||
''' get facts from the remote system '''
|
||||
|
||||
host_list = self._trim_unavailable_hosts(play._play_hosts)
|
||||
|
||||
if play.gather_facts is None and C.DEFAULT_GATHERING == 'smart':
|
||||
host_list = [h for h in host_list if h not in self.SETUP_CACHE or 'module_setup' not in self.SETUP_CACHE[h]]
|
||||
if len(host_list) == 0:
|
||||
return {}
|
||||
elif play.gather_facts is False or (play.gather_facts is None and C.DEFAULT_GATHERING == 'explicit'):
|
||||
return {}
|
||||
|
||||
self.callbacks.on_setup()
|
||||
self.inventory.restrict_to(host_list)
|
||||
|
||||
ansible.callbacks.set_task(self.callbacks, None)
|
||||
ansible.callbacks.set_task(self.runner_callbacks, None)
|
||||
|
||||
# push any variables down to the system
|
||||
setup_results = ansible.runner.Runner(
|
||||
basedir=self.basedir,
|
||||
pattern=play.hosts,
|
||||
module_name='setup',
|
||||
module_args={},
|
||||
inventory=self.inventory,
|
||||
forks=self.forks,
|
||||
module_path=self.module_path,
|
||||
timeout=self.timeout,
|
||||
remote_user=play.remote_user,
|
||||
remote_pass=self.remote_pass,
|
||||
remote_port=play.remote_port,
|
||||
private_key_file=self.private_key_file,
|
||||
setup_cache=self.SETUP_CACHE,
|
||||
vars_cache=self.VARS_CACHE,
|
||||
callbacks=self.runner_callbacks,
|
||||
become=play.become,
|
||||
become_method=play.become_method,
|
||||
become_user=play.become_user,
|
||||
become_pass=self.become_pass,
|
||||
vault_pass=self.vault_password,
|
||||
transport=play.transport,
|
||||
is_playbook=True,
|
||||
module_vars=play.vars,
|
||||
play_vars=play.vars,
|
||||
play_file_vars=play.vars_file_vars,
|
||||
role_vars=play.role_vars,
|
||||
default_vars=play.default_vars,
|
||||
check=self.check,
|
||||
diff=self.diff,
|
||||
accelerate=play.accelerate,
|
||||
accelerate_port=play.accelerate_port,
|
||||
).run()
|
||||
self.stats.compute(setup_results, setup=True)
|
||||
|
||||
self.inventory.lift_restriction()
|
||||
|
||||
# now for each result, load into the setup cache so we can
|
||||
# let runner template out future commands
|
||||
setup_ok = setup_results.get('contacted', {})
|
||||
for (host, result) in setup_ok.iteritems():
|
||||
utils.update_hash(self.SETUP_CACHE, host, {'module_setup': True})
|
||||
utils.update_hash(self.SETUP_CACHE, host, result.get('ansible_facts', {}))
|
||||
return setup_results
|
||||
|
||||
# *****************************************************
|
||||
|
||||
|
||||
def generate_retry_inventory(self, replay_hosts):
|
||||
'''
|
||||
called by /usr/bin/ansible when a playbook run fails. It generates an inventory
|
||||
that allows re-running on ONLY the failed hosts. This may duplicate some
|
||||
variable information in group_vars/host_vars but that is ok, and expected.
|
||||
'''
|
||||
|
||||
buf = StringIO.StringIO()
|
||||
for x in replay_hosts:
|
||||
buf.write("%s\n" % x)
|
||||
basedir = C.shell_expand_path(C.RETRY_FILES_SAVE_PATH)
|
||||
filename = "%s.retry" % os.path.basename(self.filename)
|
||||
filename = filename.replace(".yml","")
|
||||
filename = os.path.join(basedir, filename)
|
||||
|
||||
try:
|
||||
if not os.path.exists(basedir):
|
||||
os.makedirs(basedir)
|
||||
|
||||
fd = open(filename, 'w')
|
||||
fd.write(buf.getvalue())
|
||||
fd.close()
|
||||
except:
|
||||
ansible.callbacks.display(
|
||||
"\nERROR: could not create retry file. Check the value of \n"
|
||||
+ "the configuration variable 'retry_files_save_path' or set \n"
|
||||
+ "'retry_files_enabled' to False to avoid this message.\n",
|
||||
color='red'
|
||||
)
|
||||
return None
|
||||
|
||||
return filename
|
||||
|
||||
# *****************************************************
|
||||
def tasks_to_run_in_play(self, play):
|
||||
|
||||
tasks = []
|
||||
|
||||
for task in play.tasks():
|
||||
# only run the task if the requested tags match or has 'always' tag
|
||||
u = set(['untagged'])
|
||||
task_set = set(task.tags)
|
||||
|
||||
if 'always' in task.tags:
|
||||
should_run = True
|
||||
else:
|
||||
if 'all' in self.only_tags:
|
||||
should_run = True
|
||||
else:
|
||||
should_run = False
|
||||
if 'tagged' in self.only_tags:
|
||||
if task_set != u:
|
||||
should_run = True
|
||||
elif 'untagged' in self.only_tags:
|
||||
if task_set == u:
|
||||
should_run = True
|
||||
else:
|
||||
if task_set.intersection(self.only_tags):
|
||||
should_run = True
|
||||
|
||||
# Check for tags that we need to skip
|
||||
if 'all' in self.skip_tags:
|
||||
should_run = False
|
||||
else:
|
||||
if 'tagged' in self.skip_tags:
|
||||
if task_set != u:
|
||||
should_run = False
|
||||
elif 'untagged' in self.skip_tags:
|
||||
if task_set == u:
|
||||
should_run = False
|
||||
else:
|
||||
if should_run:
|
||||
if task_set.intersection(self.skip_tags):
|
||||
should_run = False
|
||||
|
||||
if should_run:
|
||||
tasks.append(task)
|
||||
|
||||
return tasks
|
||||
|
||||
# *****************************************************
|
||||
def _run_play(self, play):
|
||||
''' run a list of tasks for a given pattern, in order '''
|
||||
|
||||
self.callbacks.on_play_start(play.name)
|
||||
# Get the hosts for this play
|
||||
play._play_hosts = self.inventory.list_hosts(play.hosts)
|
||||
# if no hosts matches this play, drop out
|
||||
if not play._play_hosts:
|
||||
self.callbacks.on_no_hosts_matched()
|
||||
return True
|
||||
|
||||
# get facts from system
|
||||
self._do_setup_step(play)
|
||||
|
||||
# now with that data, handle contentional variable file imports!
|
||||
all_hosts = self._trim_unavailable_hosts(play._play_hosts)
|
||||
play.update_vars_files(all_hosts, vault_password=self.vault_password)
|
||||
hosts_count = len(all_hosts)
|
||||
|
||||
if play.serial.endswith("%"):
|
||||
|
||||
# This is a percentage, so calculate it based on the
|
||||
# number of hosts
|
||||
serial_pct = int(play.serial.replace("%",""))
|
||||
serial = int((serial_pct/100.0) * len(all_hosts))
|
||||
|
||||
# Ensure that no matter how small the percentage, serial
|
||||
# can never fall below 1, so that things actually happen
|
||||
serial = max(serial, 1)
|
||||
else:
|
||||
serial = int(play.serial)
|
||||
|
||||
serialized_batch = []
|
||||
if serial <= 0:
|
||||
serialized_batch = [all_hosts]
|
||||
else:
|
||||
# do N forks all the way through before moving to next
|
||||
while len(all_hosts) > 0:
|
||||
play_hosts = []
|
||||
for x in range(serial):
|
||||
if len(all_hosts) > 0:
|
||||
play_hosts.append(all_hosts.pop(0))
|
||||
serialized_batch.append(play_hosts)
|
||||
|
||||
task_errors = False
|
||||
for on_hosts in serialized_batch:
|
||||
|
||||
# restrict the play to just the hosts we have in our on_hosts block that are
|
||||
# available.
|
||||
play._play_hosts = self._trim_unavailable_hosts(on_hosts)
|
||||
self.inventory.also_restrict_to(on_hosts)
|
||||
|
||||
for task in self.tasks_to_run_in_play(play):
|
||||
|
||||
if task.meta is not None:
|
||||
# meta tasks can force handlers to run mid-play
|
||||
if task.meta == 'flush_handlers':
|
||||
self.run_handlers(play)
|
||||
|
||||
# skip calling the handler till the play is finished
|
||||
continue
|
||||
|
||||
if not self._run_task(play, task, False):
|
||||
# whether no hosts matched is fatal or not depends if it was on the initial step.
|
||||
# if we got exactly no hosts on the first step (setup!) then the host group
|
||||
# just didn't match anything and that's ok
|
||||
return False
|
||||
|
||||
# Get a new list of what hosts are left as available, the ones that
|
||||
# did not go fail/dark during the task
|
||||
host_list = self._trim_unavailable_hosts(play._play_hosts)
|
||||
|
||||
# Set max_fail_pct to 0, So if any hosts fails, bail out
|
||||
if task.any_errors_fatal and len(host_list) < hosts_count:
|
||||
play.max_fail_pct = 0
|
||||
|
||||
# If threshold for max nodes failed is exceeded, bail out.
|
||||
if play.serial > 0:
|
||||
# if serial is set, we need to shorten the size of host_count
|
||||
play_count = len(play._play_hosts)
|
||||
if (play_count - len(host_list)) > int((play.max_fail_pct)/100.0 * play_count):
|
||||
host_list = None
|
||||
else:
|
||||
if (hosts_count - len(host_list)) > int((play.max_fail_pct)/100.0 * hosts_count):
|
||||
host_list = None
|
||||
|
||||
# if no hosts remain, drop out
|
||||
if not host_list:
|
||||
if play.force_handlers:
|
||||
task_errors = True
|
||||
break
|
||||
else:
|
||||
self.callbacks.on_no_hosts_remaining()
|
||||
return False
|
||||
|
||||
# lift restrictions after each play finishes
|
||||
self.inventory.lift_also_restriction()
|
||||
|
||||
if task_errors and not play.force_handlers:
|
||||
# if there were failed tasks and handler execution
|
||||
# is not forced, quit the play with an error
|
||||
return False
|
||||
else:
|
||||
# no errors, go ahead and execute all handlers
|
||||
if not self.run_handlers(play):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def run_handlers(self, play):
|
||||
on_hosts = play._play_hosts
|
||||
hosts_count = len(on_hosts)
|
||||
for task in play.tasks():
|
||||
if task.meta is not None:
|
||||
|
||||
fired_names = {}
|
||||
for handler in play.handlers():
|
||||
if len(handler.notified_by) > 0:
|
||||
self.inventory.restrict_to(handler.notified_by)
|
||||
|
||||
# Resolve the variables first
|
||||
handler_name = template(play.basedir, handler.name, handler.module_vars)
|
||||
if handler_name not in fired_names:
|
||||
self._run_task(play, handler, True)
|
||||
# prevent duplicate handler includes from running more than once
|
||||
fired_names[handler_name] = 1
|
||||
|
||||
host_list = self._trim_unavailable_hosts(play._play_hosts)
|
||||
if handler.any_errors_fatal and len(host_list) < hosts_count:
|
||||
play.max_fail_pct = 0
|
||||
if (hosts_count - len(host_list)) > int((play.max_fail_pct)/100.0 * hosts_count):
|
||||
host_list = None
|
||||
if not host_list and not play.force_handlers:
|
||||
self.callbacks.on_no_hosts_remaining()
|
||||
return False
|
||||
|
||||
self.inventory.lift_restriction()
|
||||
new_list = handler.notified_by[:]
|
||||
for host in handler.notified_by:
|
||||
if host in on_hosts:
|
||||
while host in new_list:
|
||||
new_list.remove(host)
|
||||
handler.notified_by = new_list
|
||||
|
||||
continue
|
||||
|
||||
return True
|
||||
def get_plays(self):
|
||||
return self._entries[:]
|
||||
|
||||
@@ -15,21 +15,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class AnsibleError(Exception):
|
||||
''' The base Ansible exception from which all others should subclass '''
|
||||
pass
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
class AnsibleFileNotFound(AnsibleError):
|
||||
pass
|
||||
class Attribute:
|
||||
|
||||
class AnsibleConnectionFailed(AnsibleError):
|
||||
pass
|
||||
def __init__(self, isa=None, private=False, default=None, required=False):
|
||||
|
||||
class AnsibleYAMLValidationFailed(AnsibleError):
|
||||
pass
|
||||
self.isa = isa
|
||||
self.private = private
|
||||
self.default = default
|
||||
self.required = required
|
||||
|
||||
class AnsibleUndefinedVariable(AnsibleError):
|
||||
pass
|
||||
|
||||
class AnsibleFilterError(AnsibleError):
|
||||
class FieldAttribute(Attribute):
|
||||
pass
|
||||
345
lib/ansible/playbook/base.py
Normal file
345
lib/ansible/playbook/base.py
Normal file
@@ -0,0 +1,345 @@
|
||||
# (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 uuid
|
||||
|
||||
from functools import partial
|
||||
from inspect import getmembers
|
||||
from io import FileIO
|
||||
|
||||
from six import iteritems, string_types
|
||||
|
||||
from jinja2.exceptions import UndefinedError
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.parsing import DataLoader
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.template import Templar
|
||||
from ansible.utils.boolean import boolean
|
||||
|
||||
from ansible.utils.debug import debug
|
||||
|
||||
from ansible.template import template
|
||||
|
||||
class Base:
|
||||
|
||||
# connection/transport
|
||||
_connection = FieldAttribute(isa='string')
|
||||
_port = FieldAttribute(isa='int')
|
||||
_remote_user = FieldAttribute(isa='string')
|
||||
|
||||
# vars and flags
|
||||
_vars = FieldAttribute(isa='dict', default=dict())
|
||||
_environment = FieldAttribute(isa='dict', default=dict())
|
||||
_no_log = FieldAttribute(isa='bool', default=False)
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# initialize the data loader and variable manager, which will be provided
|
||||
# later when the object is actually loaded
|
||||
self._loader = None
|
||||
self._variable_manager = None
|
||||
|
||||
# every object gets a random uuid:
|
||||
self._uuid = uuid.uuid4()
|
||||
|
||||
# and initialize the base attributes
|
||||
self._initialize_base_attributes()
|
||||
|
||||
# The following three functions are used to programatically define data
|
||||
# descriptors (aka properties) for the Attributes of all of the playbook
|
||||
# objects (tasks, blocks, plays, etc).
|
||||
#
|
||||
# The function signature is a little strange because of how we define
|
||||
# them. We use partial to give each method the name of the Attribute that
|
||||
# it is for. Since partial prefills the positional arguments at the
|
||||
# beginning of the function we end up with the first positional argument
|
||||
# being allocated to the name instead of to the class instance (self) as
|
||||
# normal. To deal with that we make the property name field the first
|
||||
# positional argument and self the second arg.
|
||||
#
|
||||
# Because these methods are defined inside of the class, they get bound to
|
||||
# the instance when the object is created. After we run partial on them
|
||||
# and put the result back into the class as a property, they get bound
|
||||
# a second time. This leads to self being placed in the arguments twice.
|
||||
# To work around that, we mark the functions as @staticmethod so that the
|
||||
# first binding to the instance doesn't happen.
|
||||
|
||||
@staticmethod
|
||||
def _generic_g(prop_name, self):
|
||||
method = "_get_attr_%s" % prop_name
|
||||
if method in dir(self):
|
||||
return getattr(self, method)()
|
||||
|
||||
return self._attributes[prop_name]
|
||||
|
||||
@staticmethod
|
||||
def _generic_s(prop_name, self, value):
|
||||
self._attributes[prop_name] = value
|
||||
|
||||
@staticmethod
|
||||
def _generic_d(prop_name, self):
|
||||
del self._attributes[prop_name]
|
||||
|
||||
def _get_base_attributes(self):
|
||||
'''
|
||||
Returns the list of attributes for this class (or any subclass thereof).
|
||||
If the attribute name starts with an underscore, it is removed
|
||||
'''
|
||||
base_attributes = dict()
|
||||
for (name, value) in getmembers(self.__class__):
|
||||
if isinstance(value, Attribute):
|
||||
if name.startswith('_'):
|
||||
name = name[1:]
|
||||
base_attributes[name] = value
|
||||
return base_attributes
|
||||
|
||||
def _initialize_base_attributes(self):
|
||||
# each class knows attributes set upon it, see Task.py for example
|
||||
self._attributes = dict()
|
||||
|
||||
for (name, value) in self._get_base_attributes().items():
|
||||
getter = partial(self._generic_g, name)
|
||||
setter = partial(self._generic_s, name)
|
||||
deleter = partial(self._generic_d, name)
|
||||
|
||||
# Place the property into the class so that cls.name is the
|
||||
# property functions.
|
||||
setattr(Base, name, property(getter, setter, deleter))
|
||||
|
||||
# Place the value into the instance so that the property can
|
||||
# process and hold that value/
|
||||
setattr(self, name, value.default)
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
''' infrequently used method to do some pre-processing of legacy terms '''
|
||||
|
||||
for base_class in self.__class__.mro():
|
||||
method = getattr(self, "_preprocess_data_%s" % base_class.__name__.lower(), None)
|
||||
if method:
|
||||
return method(ds)
|
||||
return ds
|
||||
|
||||
def load_data(self, ds, variable_manager=None, loader=None):
|
||||
''' walk the input datastructure and assign any values '''
|
||||
|
||||
assert ds is not None
|
||||
|
||||
# the variable manager class is used to manage and merge variables
|
||||
# down to a single dictionary for reference in templating, etc.
|
||||
self._variable_manager = variable_manager
|
||||
|
||||
# the data loader class is used to parse data from strings and files
|
||||
if loader is not None:
|
||||
self._loader = loader
|
||||
else:
|
||||
self._loader = DataLoader()
|
||||
|
||||
if isinstance(ds, string_types) or isinstance(ds, FileIO):
|
||||
ds = self._loader.load(ds)
|
||||
|
||||
# call the preprocess_data() function to massage the data into
|
||||
# something we can more easily parse, and then call the validation
|
||||
# function on it to ensure there are no incorrect key values
|
||||
ds = self.preprocess_data(ds)
|
||||
self._validate_attributes(ds)
|
||||
|
||||
# Walk all attributes in the class.
|
||||
#
|
||||
# FIXME: we currently don't do anything with private attributes but
|
||||
# may later decide to filter them out of 'ds' here.
|
||||
|
||||
for name in self._get_base_attributes():
|
||||
# copy the value over unless a _load_field method is defined
|
||||
if name in ds:
|
||||
method = getattr(self, '_load_%s' % name, None)
|
||||
if method:
|
||||
self._attributes[name] = method(name, ds[name])
|
||||
else:
|
||||
self._attributes[name] = ds[name]
|
||||
|
||||
# run early, non-critical validation
|
||||
self.validate()
|
||||
|
||||
# cache the datastructure internally
|
||||
setattr(self, '_ds', ds)
|
||||
|
||||
# return the constructed object
|
||||
return self
|
||||
|
||||
def get_ds(self):
|
||||
try:
|
||||
return getattr(self, '_ds')
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_loader(self):
|
||||
return self._loader
|
||||
|
||||
def get_variable_manager(self):
|
||||
return self._variable_manager
|
||||
|
||||
def _validate_attributes(self, ds):
|
||||
'''
|
||||
Ensures that there are no keys in the datastructure which do
|
||||
not map to attributes for this object.
|
||||
'''
|
||||
|
||||
valid_attrs = frozenset(name for name in self._get_base_attributes())
|
||||
for key in ds:
|
||||
if key not in valid_attrs:
|
||||
raise AnsibleParserError("'%s' is not a valid attribute for a %s" % (key, self.__class__.__name__), obj=ds)
|
||||
|
||||
def validate(self, all_vars=dict()):
|
||||
''' validation that is done at parse time, not load time '''
|
||||
|
||||
# walk all fields in the object
|
||||
for (name, attribute) in iteritems(self._get_base_attributes()):
|
||||
|
||||
# run validator only if present
|
||||
method = getattr(self, '_validate_%s' % name, None)
|
||||
if method:
|
||||
method(attribute, name, getattr(self, name))
|
||||
|
||||
def copy(self):
|
||||
'''
|
||||
Create a copy of this object and return it.
|
||||
'''
|
||||
|
||||
new_me = self.__class__()
|
||||
|
||||
for name in self._get_base_attributes():
|
||||
setattr(new_me, name, getattr(self, name))
|
||||
|
||||
new_me._loader = self._loader
|
||||
new_me._variable_manager = self._variable_manager
|
||||
|
||||
return new_me
|
||||
|
||||
def post_validate(self, templar):
|
||||
'''
|
||||
we can't tell that everything is of the right type until we have
|
||||
all the variables. Run basic types (from isa) as well as
|
||||
any _post_validate_<foo> functions.
|
||||
'''
|
||||
|
||||
basedir = None
|
||||
if self._loader is not None:
|
||||
basedir = self._loader.get_basedir()
|
||||
|
||||
for (name, attribute) in iteritems(self._get_base_attributes()):
|
||||
|
||||
if getattr(self, name) is None:
|
||||
if not attribute.required:
|
||||
continue
|
||||
else:
|
||||
raise AnsibleParserError("the field '%s' is required but was not set" % name)
|
||||
|
||||
try:
|
||||
# if the attribute contains a variable, template it now
|
||||
value = templar.template(getattr(self, name))
|
||||
|
||||
# run the post-validator if present
|
||||
method = getattr(self, '_post_validate_%s' % name, None)
|
||||
if method:
|
||||
value = method(attribute, value, all_vars, templar._fail_on_undefined_errors)
|
||||
else:
|
||||
# otherwise, just make sure the attribute is of the type it should be
|
||||
if attribute.isa == 'string':
|
||||
value = unicode(value)
|
||||
elif attribute.isa == 'int':
|
||||
value = int(value)
|
||||
elif attribute.isa == 'bool':
|
||||
value = boolean(value)
|
||||
elif attribute.isa == 'list':
|
||||
if not isinstance(value, list):
|
||||
value = [ value ]
|
||||
elif attribute.isa == 'dict' and not isinstance(value, dict):
|
||||
raise TypeError()
|
||||
|
||||
# and assign the massaged value back to the attribute field
|
||||
setattr(self, name, value)
|
||||
|
||||
except (TypeError, ValueError) as e:
|
||||
raise AnsibleParserError("the field '%s' has an invalid value (%s), and could not be converted to an %s. Error was: %s" % (name, value, attribute.isa, e), obj=self.get_ds())
|
||||
except UndefinedError as e:
|
||||
if templar._fail_on_undefined_errors:
|
||||
raise AnsibleParserError("the field '%s' has an invalid value, which appears to include a variable that is undefined. The error was: %s" % (name,e), obj=self.get_ds())
|
||||
|
||||
def serialize(self):
|
||||
'''
|
||||
Serializes the object derived from the base object into
|
||||
a dictionary of values. This only serializes the field
|
||||
attributes for the object, so this may need to be overridden
|
||||
for any classes which wish to add additional items not stored
|
||||
as field attributes.
|
||||
'''
|
||||
|
||||
repr = dict()
|
||||
|
||||
for name in self._get_base_attributes():
|
||||
repr[name] = getattr(self, name)
|
||||
|
||||
# serialize the uuid field
|
||||
repr['uuid'] = getattr(self, '_uuid')
|
||||
|
||||
return repr
|
||||
|
||||
def deserialize(self, data):
|
||||
'''
|
||||
Given a dictionary of values, load up the field attributes for
|
||||
this object. As with serialize(), if there are any non-field
|
||||
attribute data members, this method will need to be overridden
|
||||
and extended.
|
||||
'''
|
||||
|
||||
assert isinstance(data, dict)
|
||||
|
||||
for (name, attribute) in iteritems(self._get_base_attributes()):
|
||||
if name in data:
|
||||
setattr(self, name, data[name])
|
||||
else:
|
||||
setattr(self, name, attribute.default)
|
||||
|
||||
# restore the UUID field
|
||||
setattr(self, '_uuid', data.get('uuid'))
|
||||
|
||||
def _extend_value(self, value, new_value):
|
||||
'''
|
||||
Will extend the value given with new_value (and will turn both
|
||||
into lists if they are not so already). The values are run through
|
||||
a set to remove duplicate values.
|
||||
'''
|
||||
|
||||
if not isinstance(value, list):
|
||||
value = [ value ]
|
||||
if not isinstance(new_value, list):
|
||||
new_value = [ new_value ]
|
||||
|
||||
return list(set(value + new_value))
|
||||
|
||||
def __getstate__(self):
|
||||
return self.serialize()
|
||||
|
||||
def __setstate__(self, data):
|
||||
self.__init__()
|
||||
self.deserialize(data)
|
||||
|
||||
141
lib/ansible/playbook/become.py
Normal file
141
lib/ansible/playbook/become.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# (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 import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
#from ansible.utils.display import deprecated
|
||||
|
||||
class Become:
|
||||
|
||||
# Privlege escalation
|
||||
_become = FieldAttribute(isa='bool', default=False)
|
||||
_become_method = FieldAttribute(isa='string')
|
||||
_become_user = FieldAttribute(isa='string')
|
||||
_become_pass = FieldAttribute(isa='string')
|
||||
|
||||
def __init__(self):
|
||||
return super(Become, self).__init__()
|
||||
|
||||
def _detect_privilege_escalation_conflict(self, ds):
|
||||
|
||||
# Fail out if user specifies conflicting privilege escalations
|
||||
has_become = 'become' in ds or 'become_user'in ds
|
||||
has_sudo = 'sudo' in ds or 'sudo_user' in ds
|
||||
has_su = 'su' in ds or 'su_user' in ds
|
||||
|
||||
if has_become:
|
||||
msg = 'The become params ("become", "become_user") and'
|
||||
if has_sudo:
|
||||
raise AnsibleParserError('%s sudo params ("sudo", "sudo_user") cannot be used together' % msg)
|
||||
elif has_su:
|
||||
raise AnsibleParserError('%s su params ("su", "su_user") cannot be used together' % msg)
|
||||
elif has_sudo and has_su:
|
||||
raise AnsibleParserError('sudo params ("sudo", "sudo_user") and su params ("su", "su_user") cannot be used together')
|
||||
|
||||
def _preprocess_data_become(self, ds):
|
||||
"""Preprocess the playbook data for become attributes
|
||||
|
||||
This is called from the Base object's preprocess_data() method which
|
||||
in turn is called pretty much anytime any sort of playbook object
|
||||
(plays, tasks, blocks, etc) are created.
|
||||
"""
|
||||
|
||||
self._detect_privilege_escalation_conflict(ds)
|
||||
|
||||
# Setting user implies setting become/sudo/su to true
|
||||
if 'become_user' in ds and not ds.get('become', False):
|
||||
ds['become'] = True
|
||||
|
||||
# Privilege escalation, backwards compatibility for sudo/su
|
||||
if 'sudo' in ds or 'sudo_user' in ds:
|
||||
ds['become_method'] = 'sudo'
|
||||
if 'sudo' in ds:
|
||||
ds['become'] = ds['sudo']
|
||||
del ds['sudo']
|
||||
else:
|
||||
ds['become'] = True
|
||||
if 'sudo_user' in ds:
|
||||
ds['become_user'] = ds['sudo_user']
|
||||
del ds['sudo_user']
|
||||
|
||||
#deprecated("Instead of sudo/sudo_user, use become/become_user and set become_method to 'sudo' (default)")
|
||||
|
||||
elif 'su' in ds or 'su_user' in ds:
|
||||
ds['become_method'] = 'su'
|
||||
if 'su' in ds:
|
||||
ds['become'] = ds['su']
|
||||
del ds['su']
|
||||
else:
|
||||
ds['become'] = True
|
||||
if 'su_user' in ds:
|
||||
ds['become_user'] = ds['su_user']
|
||||
del ds['su_user']
|
||||
|
||||
#deprecated("Instead of su/su_user, use become/become_user and set become_method to 'su' (default is sudo)")
|
||||
|
||||
# if we are becoming someone else, but some fields are unset,
|
||||
# make sure they're initialized to the default config values
|
||||
if ds.get('become', False):
|
||||
if ds.get('become_method', None) is None:
|
||||
ds['become_method'] = C.DEFAULT_BECOME_METHOD
|
||||
if ds.get('become_user', None) is None:
|
||||
ds['become_user'] = C.DEFAULT_BECOME_USER
|
||||
|
||||
return ds
|
||||
|
||||
def _get_attr_become(self):
|
||||
'''
|
||||
Override for the 'become' getattr fetcher, used from Base.
|
||||
'''
|
||||
if hasattr(self, '_get_parent_attribute'):
|
||||
return self._get_parent_attribute('become')
|
||||
else:
|
||||
return self._attributes['become']
|
||||
|
||||
def _get_attr_become_method(self):
|
||||
'''
|
||||
Override for the 'become_method' getattr fetcher, used from Base.
|
||||
'''
|
||||
if hasattr(self, '_get_parent_attribute'):
|
||||
return self._get_parent_attribute('become_method')
|
||||
else:
|
||||
return self._attributes['become_method']
|
||||
|
||||
def _get_attr_become_user(self):
|
||||
'''
|
||||
Override for the 'become_user' getattr fetcher, used from Base.
|
||||
'''
|
||||
if hasattr(self, '_get_parent_attribute'):
|
||||
return self._get_parent_attribute('become_user')
|
||||
else:
|
||||
return self._attributes['become_user']
|
||||
|
||||
def _get_attr_become_password(self):
|
||||
'''
|
||||
Override for the 'become_password' getattr fetcher, used from Base.
|
||||
'''
|
||||
if hasattr(self, '_get_parent_attribute'):
|
||||
return self._get_parent_attribute('become_password')
|
||||
else:
|
||||
return self._attributes['become_password']
|
||||
|
||||
|
||||
324
lib/ansible/playbook/block.py
Normal file
324
lib/ansible/playbook/block.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# (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.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.playbook.base import Base
|
||||
from ansible.playbook.become import Become
|
||||
from ansible.playbook.conditional import Conditional
|
||||
from ansible.playbook.helpers import load_list_of_tasks
|
||||
from ansible.playbook.role import Role
|
||||
from ansible.playbook.taggable import Taggable
|
||||
|
||||
class Block(Base, Become, Conditional, Taggable):
|
||||
|
||||
_block = FieldAttribute(isa='list', default=[])
|
||||
_rescue = FieldAttribute(isa='list', default=[])
|
||||
_always = FieldAttribute(isa='list', default=[])
|
||||
|
||||
# for future consideration? this would be functionally
|
||||
# similar to the 'else' clause for exceptions
|
||||
#_otherwise = FieldAttribute(isa='list')
|
||||
|
||||
def __init__(self, play=None, parent_block=None, role=None, task_include=None, use_handlers=False):
|
||||
self._play = play
|
||||
self._role = role
|
||||
self._task_include = task_include
|
||||
self._parent_block = parent_block
|
||||
self._use_handlers = use_handlers
|
||||
self._dep_chain = []
|
||||
|
||||
super(Block, self).__init__()
|
||||
|
||||
def get_vars(self):
|
||||
'''
|
||||
Blocks do not store variables directly, however they may be a member
|
||||
of a role or task include which does, so return those if present.
|
||||
'''
|
||||
|
||||
all_vars = dict()
|
||||
|
||||
if self._role:
|
||||
all_vars.update(self._role.get_vars())
|
||||
if self._parent_block:
|
||||
all_vars.update(self._parent_block.get_vars())
|
||||
if self._task_include:
|
||||
all_vars.update(self._task_include.get_vars())
|
||||
|
||||
all_vars.update(self.vars)
|
||||
return all_vars
|
||||
|
||||
@staticmethod
|
||||
def load(data, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None):
|
||||
b = Block(play=play, parent_block=parent_block, role=role, task_include=task_include, use_handlers=use_handlers)
|
||||
return b.load_data(data, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
'''
|
||||
If a simple task is given, an implicit block for that single task
|
||||
is created, which goes in the main portion of the block
|
||||
'''
|
||||
|
||||
is_block = False
|
||||
for attr in ('block', 'rescue', 'always'):
|
||||
if attr in ds:
|
||||
is_block = True
|
||||
break
|
||||
|
||||
if not is_block:
|
||||
if isinstance(ds, list):
|
||||
return super(Block, self).preprocess_data(dict(block=ds))
|
||||
else:
|
||||
return super(Block, self).preprocess_data(dict(block=[ds]))
|
||||
|
||||
return super(Block, self).preprocess_data(ds)
|
||||
|
||||
def _load_block(self, attr, ds):
|
||||
return load_list_of_tasks(
|
||||
ds,
|
||||
play=self._play,
|
||||
block=self,
|
||||
role=self._role,
|
||||
task_include=self._task_include,
|
||||
variable_manager=self._variable_manager,
|
||||
loader=self._loader,
|
||||
use_handlers=self._use_handlers,
|
||||
)
|
||||
|
||||
def _load_rescue(self, attr, ds):
|
||||
return load_list_of_tasks(
|
||||
ds,
|
||||
play=self._play,
|
||||
block=self,
|
||||
role=self._role,
|
||||
task_include=self._task_include,
|
||||
variable_manager=self._variable_manager,
|
||||
loader=self._loader,
|
||||
use_handlers=self._use_handlers,
|
||||
)
|
||||
|
||||
def _load_always(self, attr, ds):
|
||||
return load_list_of_tasks(
|
||||
ds,
|
||||
play=self._play,
|
||||
block=self,
|
||||
role=self._role,
|
||||
task_include=self._task_include,
|
||||
variable_manager=self._variable_manager,
|
||||
loader=self._loader,
|
||||
use_handlers=self._use_handlers,
|
||||
)
|
||||
|
||||
# not currently used
|
||||
#def _load_otherwise(self, attr, ds):
|
||||
# return load_list_of_tasks(
|
||||
# ds,
|
||||
# play=self._play,
|
||||
# block=self,
|
||||
# role=self._role,
|
||||
# task_include=self._task_include,
|
||||
# variable_manager=self._variable_manager,
|
||||
# loader=self._loader,
|
||||
# use_handlers=self._use_handlers,
|
||||
# )
|
||||
|
||||
def copy(self, exclude_parent=False):
|
||||
def _dupe_task_list(task_list, new_block):
|
||||
new_task_list = []
|
||||
for task in task_list:
|
||||
if isinstance(task, Block):
|
||||
new_task = task.copy(exclude_parent=True)
|
||||
new_task._parent_block = new_block
|
||||
else:
|
||||
new_task = task.copy(exclude_block=True)
|
||||
new_task._block = new_block
|
||||
new_task_list.append(new_task)
|
||||
return new_task_list
|
||||
|
||||
new_me = super(Block, self).copy()
|
||||
new_me._play = self._play
|
||||
new_me._use_handlers = self._use_handlers
|
||||
new_me._dep_chain = self._dep_chain[:]
|
||||
|
||||
new_me.block = _dupe_task_list(self.block or [], new_me)
|
||||
new_me.rescue = _dupe_task_list(self.rescue or [], new_me)
|
||||
new_me.always = _dupe_task_list(self.always or [], new_me)
|
||||
|
||||
new_me._parent_block = None
|
||||
if self._parent_block and not exclude_parent:
|
||||
new_me._parent_block = self._parent_block.copy()
|
||||
|
||||
new_me._role = None
|
||||
if self._role:
|
||||
new_me._role = self._role
|
||||
|
||||
new_me._task_include = None
|
||||
if self._task_include:
|
||||
new_me._task_include = self._task_include.copy()
|
||||
|
||||
return new_me
|
||||
|
||||
def serialize(self):
|
||||
'''
|
||||
Override of the default serialize method, since when we're serializing
|
||||
a task we don't want to include the attribute list of tasks.
|
||||
'''
|
||||
|
||||
data = dict()
|
||||
for attr in self._get_base_attributes():
|
||||
if attr not in ('block', 'rescue', 'always'):
|
||||
data[attr] = getattr(self, attr)
|
||||
|
||||
data['dep_chain'] = self._dep_chain
|
||||
|
||||
if self._role is not None:
|
||||
data['role'] = self._role.serialize()
|
||||
if self._task_include is not None:
|
||||
data['task_include'] = self._task_include.serialize()
|
||||
|
||||
return data
|
||||
|
||||
def deserialize(self, data):
|
||||
'''
|
||||
Override of the default deserialize method, to match the above overridden
|
||||
serialize method
|
||||
'''
|
||||
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
# we don't want the full set of attributes (the task lists), as that
|
||||
# would lead to a serialize/deserialize loop
|
||||
for attr in self._get_base_attributes():
|
||||
if attr in data and attr not in ('block', 'rescue', 'always'):
|
||||
setattr(self, attr, data.get(attr))
|
||||
|
||||
self._dep_chain = data.get('dep_chain', [])
|
||||
|
||||
# if there was a serialized role, unpack it too
|
||||
role_data = data.get('role')
|
||||
if role_data:
|
||||
r = Role()
|
||||
r.deserialize(role_data)
|
||||
self._role = r
|
||||
|
||||
# if there was a serialized task include, unpack it too
|
||||
ti_data = data.get('task_include')
|
||||
if ti_data:
|
||||
ti = Task()
|
||||
ti.deserialize(ti_data)
|
||||
self._task_include = ti
|
||||
|
||||
def evaluate_conditional(self, templar, all_vars):
|
||||
if len(self._dep_chain):
|
||||
for dep in self._dep_chain:
|
||||
if not dep.evaluate_conditional(templar, all_vars):
|
||||
return False
|
||||
if self._task_include is not None:
|
||||
if not self._task_include.evaluate_conditional(templar, all_vars):
|
||||
return False
|
||||
if self._parent_block is not None:
|
||||
if not self._parent_block.evaluate_conditional(templar, all_vars):
|
||||
return False
|
||||
elif self._role is not None:
|
||||
if not self._role.evaluate_conditional(templar, all_vars):
|
||||
return False
|
||||
return super(Block, self).evaluate_conditional(templar, all_vars)
|
||||
|
||||
def set_loader(self, loader):
|
||||
self._loader = loader
|
||||
if self._parent_block:
|
||||
self._parent_block.set_loader(loader)
|
||||
elif self._role:
|
||||
self._role.set_loader(loader)
|
||||
|
||||
if self._task_include:
|
||||
self._task_include.set_loader(loader)
|
||||
|
||||
for dep in self._dep_chain:
|
||||
dep.set_loader(loader)
|
||||
|
||||
def _get_parent_attribute(self, attr, extend=False):
|
||||
'''
|
||||
Generic logic to get the attribute or parent attribute for a block value.
|
||||
'''
|
||||
|
||||
value = self._attributes[attr]
|
||||
if self._parent_block and (not value or extend):
|
||||
parent_value = getattr(self._parent_block, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, parent_value)
|
||||
else:
|
||||
value = parent_value
|
||||
if self._task_include and (not value or extend):
|
||||
parent_value = getattr(self._task_include, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, parent_value)
|
||||
else:
|
||||
value = parent_value
|
||||
if self._role and (not value or extend):
|
||||
parent_value = getattr(self._role, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, parent_value)
|
||||
else:
|
||||
value = parent_value
|
||||
|
||||
if len(self._dep_chain) and (not value or extend):
|
||||
reverse_dep_chain = self._dep_chain[:]
|
||||
reverse_dep_chain.reverse()
|
||||
for dep in reverse_dep_chain:
|
||||
dep_value = getattr(dep, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, dep_value)
|
||||
else:
|
||||
value = dep_value
|
||||
|
||||
if value and not extend:
|
||||
break
|
||||
if self._play and (not value or extend):
|
||||
parent_value = getattr(self._play, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, parent_value)
|
||||
else:
|
||||
value = parent_value
|
||||
|
||||
return value
|
||||
|
||||
def filter_tagged_tasks(self, connection_info, all_vars):
|
||||
'''
|
||||
Creates a new block, with task lists filtered based on the tags contained
|
||||
within the connection_info object.
|
||||
'''
|
||||
|
||||
def evaluate_and_append_task(target):
|
||||
tmp_list = []
|
||||
for task in target:
|
||||
if task.evaluate_tags(connection_info.only_tags, connection_info.skip_tags, all_vars=all_vars):
|
||||
tmp_list.append(task)
|
||||
return tmp_list
|
||||
|
||||
new_block = self.copy()
|
||||
new_block.block = evaluate_and_append_task(self.block)
|
||||
new_block.rescue = evaluate_and_append_task(self.rescue)
|
||||
new_block.always = evaluate_and_append_task(self.always)
|
||||
|
||||
return new_block
|
||||
|
||||
def has_tasks(self):
|
||||
return len(self.block) > 0 or len(self.rescue) > 0 or len(self.always) > 0
|
||||
102
lib/ansible/playbook/conditional.py
Normal file
102
lib/ansible/playbook/conditional.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# (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 *
|
||||
from ansible.playbook.attribute import FieldAttribute
|
||||
from ansible.template import Templar
|
||||
|
||||
class Conditional:
|
||||
|
||||
'''
|
||||
This is a mix-in class, to be used with Base to allow the object
|
||||
to be run conditionally when a condition is met or skipped.
|
||||
'''
|
||||
|
||||
_when = FieldAttribute(isa='list', default=[])
|
||||
|
||||
def __init__(self, loader=None):
|
||||
# when used directly, this class needs a loader, but we want to
|
||||
# make sure we don't trample on the existing one if this class
|
||||
# is used as a mix-in with a playbook base class
|
||||
if not hasattr(self, '_loader'):
|
||||
if loader is None:
|
||||
raise AnsibleError("a loader must be specified when using Conditional() directly")
|
||||
else:
|
||||
self._loader = loader
|
||||
super(Conditional, self).__init__()
|
||||
|
||||
def _validate_when(self, attr, name, value):
|
||||
if not isinstance(value, list):
|
||||
setattr(self, name, [ value ])
|
||||
|
||||
def evaluate_conditional(self, templar, all_vars):
|
||||
'''
|
||||
Loops through the conditionals set on this object, returning
|
||||
False if any of them evaluate as such.
|
||||
'''
|
||||
|
||||
for conditional in self.when:
|
||||
if not self._check_conditional(conditional, templar, all_vars):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _check_conditional(self, conditional, templar, all_vars):
|
||||
'''
|
||||
This method does the low-level evaluation of each conditional
|
||||
set on this object, using jinja2 to wrap the conditionals for
|
||||
evaluation.
|
||||
'''
|
||||
|
||||
original = conditional
|
||||
if conditional is None or conditional == '':
|
||||
return True
|
||||
|
||||
if conditional in all_vars and '-' not in unicode(all_vars[conditional]):
|
||||
conditional = all_vars[conditional]
|
||||
|
||||
conditional = templar.template(conditional)
|
||||
if not isinstance(conditional, basestring) or conditional == "":
|
||||
return conditional
|
||||
|
||||
# a Jinja2 evaluation that results in something Python can eval!
|
||||
presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional
|
||||
conditional = templar.template(presented)
|
||||
|
||||
val = conditional.strip()
|
||||
if val == presented:
|
||||
# the templating failed, meaning most likely a
|
||||
# variable was undefined. If we happened to be
|
||||
# looking for an undefined variable, return True,
|
||||
# otherwise fail
|
||||
if "is undefined" in original:
|
||||
return True
|
||||
elif "is defined" in original:
|
||||
return False
|
||||
else:
|
||||
raise AnsibleError("error while evaluating conditional: %s (%s)" % (original, presented))
|
||||
elif val == "True":
|
||||
return True
|
||||
elif val == "False":
|
||||
return False
|
||||
else:
|
||||
raise AnsibleError("unable to evaluate conditional: %s" % original)
|
||||
|
||||
53
lib/ansible/playbook/handler.py
Normal file
53
lib/ansible/playbook/handler.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# (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.inventory.host import Host
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
class Handler(Task):
|
||||
|
||||
def __init__(self, block=None, role=None, task_include=None):
|
||||
self._flagged_hosts = []
|
||||
|
||||
super(Handler, self).__init__(block=block, role=role, task_include=task_include)
|
||||
|
||||
def __repr__(self):
|
||||
''' returns a human readable representation of the handler '''
|
||||
return "HANDLER: %s" % self.get_name()
|
||||
|
||||
@staticmethod
|
||||
def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None):
|
||||
t = Handler(block=block, role=role, task_include=task_include)
|
||||
return t.load_data(data, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
def flag_for_host(self, host):
|
||||
#assert instanceof(host, Host)
|
||||
if host not in self._flagged_hosts:
|
||||
self._flagged_hosts.append(host)
|
||||
|
||||
def has_triggered(self, host):
|
||||
return host in self._flagged_hosts
|
||||
|
||||
def serialize(self):
|
||||
result = super(Handler, self).serialize()
|
||||
result['is_handler'] = True
|
||||
return result
|
||||
116
lib/ansible/playbook/helpers.py
Normal file
116
lib/ansible/playbook/helpers.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# (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
|
||||
|
||||
from types import NoneType
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleSequence
|
||||
|
||||
|
||||
def load_list_of_blocks(ds, play, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None):
|
||||
'''
|
||||
Given a list of mixed task/block data (parsed from YAML),
|
||||
return a list of Block() objects, where implicit blocks
|
||||
are created for each bare Task.
|
||||
'''
|
||||
|
||||
# we import here to prevent a circular dependency with imports
|
||||
from ansible.playbook.block import Block
|
||||
|
||||
assert ds is None or isinstance(ds, list), 'block has bad type: %s' % type(ds)
|
||||
|
||||
block_list = []
|
||||
if ds:
|
||||
for block in ds:
|
||||
b = Block.load(
|
||||
block,
|
||||
play=play,
|
||||
parent_block=parent_block,
|
||||
role=role,
|
||||
task_include=task_include,
|
||||
use_handlers=use_handlers,
|
||||
variable_manager=variable_manager,
|
||||
loader=loader
|
||||
)
|
||||
block_list.append(b)
|
||||
|
||||
return block_list
|
||||
|
||||
|
||||
def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None):
|
||||
'''
|
||||
Given a list of task datastructures (parsed from YAML),
|
||||
return a list of Task() or TaskInclude() objects.
|
||||
'''
|
||||
|
||||
# we import here to prevent a circular dependency with imports
|
||||
from ansible.playbook.block import Block
|
||||
from ansible.playbook.handler import Handler
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
assert isinstance(ds, list), 'task has bad type: %s' % type(ds)
|
||||
|
||||
task_list = []
|
||||
for task in ds:
|
||||
if not isinstance(task, dict):
|
||||
raise AnsibleParserError("task/handler entries must be dictionaries (got a %s)" % type(task), obj=ds)
|
||||
|
||||
if 'block' in task:
|
||||
t = Block.load(
|
||||
task,
|
||||
play=play,
|
||||
parent_block=block,
|
||||
role=role,
|
||||
task_include=task_include,
|
||||
use_handlers=use_handlers,
|
||||
variable_manager=variable_manager,
|
||||
loader=loader,
|
||||
)
|
||||
else:
|
||||
if use_handlers:
|
||||
t = Handler.load(task, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader)
|
||||
else:
|
||||
t = Task.load(task, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
task_list.append(t)
|
||||
|
||||
return task_list
|
||||
|
||||
|
||||
def load_list_of_roles(ds, current_role_path=None, variable_manager=None, loader=None):
|
||||
'''
|
||||
Loads and returns a list of RoleInclude objects from the datastructure
|
||||
list of role definitions
|
||||
'''
|
||||
|
||||
# we import here to prevent a circular dependency with imports
|
||||
from ansible.playbook.role.include import RoleInclude
|
||||
|
||||
assert isinstance(ds, list), 'roles has bad type: %s' % type(ds)
|
||||
|
||||
roles = []
|
||||
for role_def in ds:
|
||||
i = RoleInclude.load(role_def, current_role_path=current_role_path, variable_manager=variable_manager, loader=loader)
|
||||
roles.append(i)
|
||||
|
||||
return roles
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
125
lib/ansible/playbook/playbook_include.py
Normal file
125
lib/ansible/playbook/playbook_include.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# (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 ansible.parsing.splitter import split_args, parse_kv
|
||||
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
|
||||
from ansible.playbook.attribute import FieldAttribute
|
||||
from ansible.playbook.base import Base
|
||||
from ansible.playbook.taggable import Taggable
|
||||
from ansible.errors import AnsibleParserError
|
||||
|
||||
class PlaybookInclude(Base, Taggable):
|
||||
|
||||
_name = FieldAttribute(isa='string')
|
||||
_include = FieldAttribute(isa='string')
|
||||
_vars = FieldAttribute(isa='dict', default=dict())
|
||||
|
||||
@staticmethod
|
||||
def load(data, basedir, variable_manager=None, loader=None):
|
||||
return PlaybookInclude().load_data(ds=data, basedir=basedir, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
def load_data(self, ds, basedir, variable_manager=None, loader=None):
|
||||
'''
|
||||
Overrides the base load_data(), as we're actually going to return a new
|
||||
Playbook() object rather than a PlaybookInclude object
|
||||
'''
|
||||
|
||||
# import here to avoid a dependency loop
|
||||
from ansible.playbook import Playbook
|
||||
|
||||
# first, we use the original parent method to correctly load the object
|
||||
# via the load_data/preprocess_data system we normally use for other
|
||||
# playbook objects
|
||||
new_obj = super(PlaybookInclude, self).load_data(ds, variable_manager, loader)
|
||||
|
||||
# then we use the object to load a Playbook
|
||||
pb = Playbook(loader=loader)
|
||||
|
||||
file_name = new_obj.include
|
||||
if not os.path.isabs(file_name):
|
||||
file_name = os.path.join(basedir, file_name)
|
||||
|
||||
pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager)
|
||||
|
||||
# finally, update each loaded playbook entry with any variables specified
|
||||
# on the included playbook and/or any tags which may have been set
|
||||
for entry in pb._entries:
|
||||
entry.vars.update(new_obj.vars)
|
||||
entry.tags = list(set(entry.tags).union(new_obj.tags))
|
||||
|
||||
return pb
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
'''
|
||||
Regorganizes the data for a PlaybookInclude datastructure to line
|
||||
up with what we expect the proper attributes to be
|
||||
'''
|
||||
|
||||
assert isinstance(ds, dict)
|
||||
|
||||
# the new, cleaned datastructure, which will have legacy
|
||||
# items reduced to a standard structure
|
||||
new_ds = AnsibleMapping()
|
||||
if isinstance(ds, AnsibleBaseYAMLObject):
|
||||
new_ds.ansible_pos = ds.ansible_pos
|
||||
|
||||
for (k,v) in ds.iteritems():
|
||||
if k == 'include':
|
||||
self._preprocess_include(ds, new_ds, k, v)
|
||||
else:
|
||||
# some basic error checking, to make sure vars are properly
|
||||
# formatted and do not conflict with k=v parameters
|
||||
# FIXME: we could merge these instead, but controlling the order
|
||||
# in which they're encountered could be difficult
|
||||
if k == 'vars':
|
||||
if 'vars' in new_ds:
|
||||
raise AnsibleParserError("include parameters cannot be mixed with 'vars' entries for include statements", obj=ds)
|
||||
elif not isinstance(v, dict):
|
||||
raise AnsibleParserError("vars for include statements must be specified as a dictionary", obj=ds)
|
||||
new_ds[k] = v
|
||||
|
||||
return super(PlaybookInclude, self).preprocess_data(new_ds)
|
||||
|
||||
def _preprocess_include(self, ds, new_ds, k, v):
|
||||
'''
|
||||
Splits the include line up into filename and parameters
|
||||
'''
|
||||
|
||||
# The include line must include at least one item, which is the filename
|
||||
# to include. Anything after that should be regarded as a parameter to the include
|
||||
items = split_args(v)
|
||||
if len(items) == 0:
|
||||
raise AnsibleParserError("include statements must specify the file name to include", obj=ds)
|
||||
else:
|
||||
# FIXME/TODO: validate that items[0] is a file, which also
|
||||
# exists and is readable
|
||||
new_ds['include'] = items[0]
|
||||
if len(items) > 1:
|
||||
# rejoin the parameter portion of the arguments and
|
||||
# then use parse_kv() to get a dict of params back
|
||||
params = parse_kv(" ".join(items[1:]))
|
||||
if 'vars' in new_ds:
|
||||
# FIXME: see fixme above regarding merging vars
|
||||
raise AnsibleParserError("include parameters cannot be mixed with 'vars' entries for include statements", obj=ds)
|
||||
new_ds['vars'] = params
|
||||
|
||||
396
lib/ansible/playbook/role/__init__.py
Normal file
396
lib/ansible/playbook/role/__init__.py
Normal file
@@ -0,0 +1,396 @@
|
||||
# (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 import iteritems, string_types
|
||||
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from hashlib import sha1
|
||||
from types import NoneType
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.parsing import DataLoader
|
||||
from ansible.playbook.attribute import FieldAttribute
|
||||
from ansible.playbook.base import Base
|
||||
from ansible.playbook.become import Become
|
||||
from ansible.playbook.conditional import Conditional
|
||||
from ansible.playbook.helpers import load_list_of_blocks
|
||||
from ansible.playbook.role.include import RoleInclude
|
||||
from ansible.playbook.role.metadata import RoleMetadata
|
||||
from ansible.playbook.taggable import Taggable
|
||||
from ansible.plugins import get_all_plugin_loaders
|
||||
from ansible.utils.vars import combine_vars
|
||||
|
||||
|
||||
__all__ = ['Role', 'ROLE_CACHE', 'hash_params']
|
||||
|
||||
# FIXME: this should be a utility function, but can't be a member of
|
||||
# the role due to the fact that it would require the use of self
|
||||
# in a static method. This is also used in the base class for
|
||||
# strategies (ansible/plugins/strategies/__init__.py)
|
||||
def hash_params(params):
|
||||
if not isinstance(params, dict):
|
||||
return params
|
||||
else:
|
||||
s = set()
|
||||
for k,v in params.iteritems():
|
||||
if isinstance(v, dict):
|
||||
s.update((k, hash_params(v)))
|
||||
elif isinstance(v, list):
|
||||
things = []
|
||||
for item in v:
|
||||
things.append(hash_params(item))
|
||||
s.update((k, tuple(things)))
|
||||
else:
|
||||
s.update((k, v))
|
||||
return frozenset(s)
|
||||
|
||||
# The role cache is used to prevent re-loading roles, which
|
||||
# may already exist. Keys into this cache are the SHA1 hash
|
||||
# of the role definition (for dictionary definitions, this
|
||||
# will be based on the repr() of the dictionary object)
|
||||
ROLE_CACHE = dict()
|
||||
|
||||
|
||||
class Role(Base, Become, Conditional, Taggable):
|
||||
|
||||
def __init__(self):
|
||||
self._role_name = None
|
||||
self._role_path = None
|
||||
self._role_params = dict()
|
||||
self._loader = None
|
||||
|
||||
self._metadata = None
|
||||
self._play = None
|
||||
self._parents = []
|
||||
self._dependencies = []
|
||||
self._task_blocks = []
|
||||
self._handler_blocks = []
|
||||
self._default_vars = dict()
|
||||
self._role_vars = dict()
|
||||
self._had_task_run = False
|
||||
self._completed = False
|
||||
|
||||
super(Role, self).__init__()
|
||||
|
||||
def __repr__(self):
|
||||
return self.get_name()
|
||||
|
||||
def get_name(self):
|
||||
return self._role_name
|
||||
|
||||
@staticmethod
|
||||
def load(role_include, parent_role=None):
|
||||
# FIXME: add back in the role caching support
|
||||
try:
|
||||
# The ROLE_CACHE is a dictionary of role names, with each entry
|
||||
# containing another dictionary corresponding to a set of parameters
|
||||
# specified for a role as the key and the Role() object itself.
|
||||
# We use frozenset to make the dictionary hashable.
|
||||
|
||||
#hashed_params = frozenset(role_include.get_role_params().iteritems())
|
||||
hashed_params = hash_params(role_include.get_role_params())
|
||||
if role_include.role in ROLE_CACHE:
|
||||
for (entry, role_obj) in ROLE_CACHE[role_include.role].iteritems():
|
||||
if hashed_params == entry:
|
||||
if parent_role:
|
||||
role_obj.add_parent(parent_role)
|
||||
return role_obj
|
||||
|
||||
r = Role()
|
||||
r._load_role_data(role_include, parent_role=parent_role)
|
||||
|
||||
if role_include.role not in ROLE_CACHE:
|
||||
ROLE_CACHE[role_include.role] = dict()
|
||||
|
||||
ROLE_CACHE[role_include.role][hashed_params] = r
|
||||
return r
|
||||
|
||||
except RuntimeError:
|
||||
# FIXME: needs a better way to access the ds in the role include
|
||||
raise AnsibleError("A recursion loop was detected with the roles specified. Make sure child roles do not have dependencies on parent roles", obj=role_include._ds)
|
||||
|
||||
def _load_role_data(self, role_include, parent_role=None):
|
||||
self._role_name = role_include.role
|
||||
self._role_path = role_include.get_role_path()
|
||||
self._role_params = role_include.get_role_params()
|
||||
self._variable_manager = role_include.get_variable_manager()
|
||||
self._loader = role_include.get_loader()
|
||||
|
||||
if parent_role:
|
||||
self.add_parent(parent_role)
|
||||
|
||||
# copy over all field attributes, except for when and tags, which
|
||||
# are special cases and need to preserve pre-existing values
|
||||
for (attr_name, _) in iteritems(self._get_base_attributes()):
|
||||
if attr_name not in ('when', 'tags'):
|
||||
setattr(self, attr_name, getattr(role_include, attr_name))
|
||||
|
||||
current_when = getattr(self, 'when')[:]
|
||||
current_when.extend(role_include.when)
|
||||
setattr(self, 'when', current_when)
|
||||
|
||||
current_tags = getattr(self, 'tags')[:]
|
||||
current_tags.extend(role_include.tags)
|
||||
setattr(self, 'tags', current_tags)
|
||||
|
||||
# dynamically load any plugins from the role directory
|
||||
for name, obj in get_all_plugin_loaders():
|
||||
if obj.subdir:
|
||||
plugin_path = os.path.join(self._role_path, obj.subdir)
|
||||
if os.path.isdir(plugin_path):
|
||||
obj.add_directory(plugin_path)
|
||||
|
||||
# load the role's other files, if they exist
|
||||
metadata = self._load_role_yaml('meta')
|
||||
if metadata:
|
||||
self._metadata = RoleMetadata.load(metadata, owner=self, loader=self._loader)
|
||||
self._dependencies = self._load_dependencies()
|
||||
|
||||
task_data = self._load_role_yaml('tasks')
|
||||
if task_data:
|
||||
self._task_blocks = load_list_of_blocks(task_data, play=None, role=self, loader=self._loader)
|
||||
|
||||
handler_data = self._load_role_yaml('handlers')
|
||||
if handler_data:
|
||||
self._handler_blocks = load_list_of_blocks(handler_data, play=None, role=self, loader=self._loader)
|
||||
|
||||
# vars and default vars are regular dictionaries
|
||||
self._role_vars = self._load_role_yaml('vars')
|
||||
if not isinstance(self._role_vars, (dict, NoneType)):
|
||||
raise AnsibleParserError("The vars/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name)
|
||||
elif self._role_vars is None:
|
||||
self._role_vars = dict()
|
||||
|
||||
self._default_vars = self._load_role_yaml('defaults')
|
||||
if not isinstance(self._default_vars, (dict, NoneType)):
|
||||
raise AnsibleParserError("The default/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name)
|
||||
elif self._default_vars is None:
|
||||
self._default_vars = dict()
|
||||
|
||||
def _load_role_yaml(self, subdir):
|
||||
file_path = os.path.join(self._role_path, subdir)
|
||||
if self._loader.path_exists(file_path) and self._loader.is_directory(file_path):
|
||||
main_file = self._resolve_main(file_path)
|
||||
if self._loader.path_exists(main_file):
|
||||
return self._loader.load_from_file(main_file)
|
||||
return None
|
||||
|
||||
def _resolve_main(self, basepath):
|
||||
''' flexibly handle variations in main filenames '''
|
||||
possible_mains = (
|
||||
os.path.join(basepath, 'main.yml'),
|
||||
os.path.join(basepath, 'main.yaml'),
|
||||
os.path.join(basepath, 'main.json'),
|
||||
os.path.join(basepath, 'main'),
|
||||
)
|
||||
|
||||
if sum([self._loader.is_file(x) for x in possible_mains]) > 1:
|
||||
raise AnsibleError("found multiple main files at %s, only one allowed" % (basepath))
|
||||
else:
|
||||
for m in possible_mains:
|
||||
if self._loader.is_file(m):
|
||||
return m # exactly one main file
|
||||
return possible_mains[0] # zero mains (we still need to return something)
|
||||
|
||||
def _load_dependencies(self):
|
||||
'''
|
||||
Recursively loads role dependencies from the metadata list of
|
||||
dependencies, if it exists
|
||||
'''
|
||||
|
||||
deps = []
|
||||
if self._metadata:
|
||||
for role_include in self._metadata.dependencies:
|
||||
r = Role.load(role_include, parent_role=self)
|
||||
deps.append(r)
|
||||
|
||||
return deps
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# other functions
|
||||
|
||||
def add_parent(self, parent_role):
|
||||
''' adds a role to the list of this roles parents '''
|
||||
assert isinstance(parent_role, Role)
|
||||
|
||||
if parent_role not in self._parents:
|
||||
self._parents.append(parent_role)
|
||||
|
||||
def get_parents(self):
|
||||
return self._parents
|
||||
|
||||
def get_default_vars(self):
|
||||
# FIXME: get these from dependent roles too
|
||||
default_vars = dict()
|
||||
for dep in self.get_all_dependencies():
|
||||
default_vars = combine_vars(default_vars, dep.get_default_vars())
|
||||
default_vars = combine_vars(default_vars, self._default_vars)
|
||||
return default_vars
|
||||
|
||||
def get_inherited_vars(self):
|
||||
inherited_vars = dict()
|
||||
for parent in self._parents:
|
||||
inherited_vars = combine_vars(inherited_vars, parent.get_inherited_vars())
|
||||
inherited_vars = combine_vars(inherited_vars, parent._role_vars)
|
||||
inherited_vars = combine_vars(inherited_vars, parent._role_params)
|
||||
return inherited_vars
|
||||
|
||||
def get_vars(self):
|
||||
all_vars = self.get_inherited_vars()
|
||||
|
||||
for dep in self.get_all_dependencies():
|
||||
all_vars = combine_vars(all_vars, dep.get_vars())
|
||||
|
||||
all_vars = combine_vars(all_vars, self._role_vars)
|
||||
all_vars = combine_vars(all_vars, self._role_params)
|
||||
|
||||
return all_vars
|
||||
|
||||
def get_direct_dependencies(self):
|
||||
return self._dependencies[:]
|
||||
|
||||
def get_all_dependencies(self):
|
||||
'''
|
||||
Returns a list of all deps, built recursively from all child dependencies,
|
||||
in the proper order in which they should be executed or evaluated.
|
||||
'''
|
||||
|
||||
child_deps = []
|
||||
|
||||
for dep in self.get_direct_dependencies():
|
||||
for child_dep in dep.get_all_dependencies():
|
||||
child_deps.append(child_dep)
|
||||
child_deps.append(dep)
|
||||
|
||||
return child_deps
|
||||
|
||||
def get_task_blocks(self):
|
||||
return self._task_blocks[:]
|
||||
|
||||
def get_handler_blocks(self):
|
||||
return self._handler_blocks[:]
|
||||
|
||||
def has_run(self):
|
||||
'''
|
||||
Returns true if this role has been iterated over completely and
|
||||
at least one task was run
|
||||
'''
|
||||
|
||||
return self._had_task_run and self._completed
|
||||
|
||||
def compile(self, play, dep_chain=[]):
|
||||
'''
|
||||
Returns the task list for this role, which is created by first
|
||||
recursively compiling the tasks for all direct dependencies, and
|
||||
then adding on the tasks for this role.
|
||||
|
||||
The role compile() also remembers and saves the dependency chain
|
||||
with each task, so tasks know by which route they were found, and
|
||||
can correctly take their parent's tags/conditionals into account.
|
||||
'''
|
||||
|
||||
block_list = []
|
||||
|
||||
# update the dependency chain here
|
||||
new_dep_chain = dep_chain + [self]
|
||||
|
||||
deps = self.get_direct_dependencies()
|
||||
for dep in deps:
|
||||
dep_blocks = dep.compile(play=play, dep_chain=new_dep_chain)
|
||||
for dep_block in dep_blocks:
|
||||
new_dep_block = dep_block.copy()
|
||||
new_dep_block._dep_chain = new_dep_chain
|
||||
new_dep_block._play = play
|
||||
block_list.append(new_dep_block)
|
||||
|
||||
block_list.extend(self._task_blocks)
|
||||
|
||||
return block_list
|
||||
|
||||
def serialize(self, include_deps=True):
|
||||
res = super(Role, self).serialize()
|
||||
|
||||
res['_role_name'] = self._role_name
|
||||
res['_role_path'] = self._role_path
|
||||
res['_role_vars'] = self._role_vars
|
||||
res['_role_params'] = self._role_params
|
||||
res['_default_vars'] = self._default_vars
|
||||
res['_had_task_run'] = self._had_task_run
|
||||
res['_completed'] = self._completed
|
||||
|
||||
if self._metadata:
|
||||
res['_metadata'] = self._metadata.serialize()
|
||||
|
||||
if include_deps:
|
||||
deps = []
|
||||
for role in self.get_direct_dependencies():
|
||||
deps.append(role.serialize())
|
||||
res['_dependencies'] = deps
|
||||
|
||||
parents = []
|
||||
for parent in self._parents:
|
||||
parents.append(parent.serialize(include_deps=False))
|
||||
res['_parents'] = parents
|
||||
|
||||
return res
|
||||
|
||||
def deserialize(self, data, include_deps=True):
|
||||
self._role_name = data.get('_role_name', '')
|
||||
self._role_path = data.get('_role_path', '')
|
||||
self._role_vars = data.get('_role_vars', dict())
|
||||
self._role_params = data.get('_role_params', dict())
|
||||
self._default_vars = data.get('_default_vars', dict())
|
||||
self._had_task_run = data.get('_had_task_run', False)
|
||||
self._completed = data.get('_completed', False)
|
||||
|
||||
if include_deps:
|
||||
deps = []
|
||||
for dep in data.get('_dependencies', []):
|
||||
r = Role()
|
||||
r.deserialize(dep)
|
||||
deps.append(r)
|
||||
setattr(self, '_dependencies', deps)
|
||||
|
||||
parent_data = data.get('_parents', [])
|
||||
parents = []
|
||||
for parent in parent_data:
|
||||
r = Role()
|
||||
r.deserialize(parent, include_deps=False)
|
||||
parents.append(r)
|
||||
setattr(self, '_parents', parents)
|
||||
|
||||
metadata_data = data.get('_metadata')
|
||||
if metadata_data:
|
||||
m = RoleMetadata()
|
||||
m.deserialize(metadata_data)
|
||||
self._metadata = m
|
||||
|
||||
super(Role, self).deserialize(data)
|
||||
|
||||
def set_loader(self, loader):
|
||||
self._loader = loader
|
||||
for parent in self._parents:
|
||||
parent.set_loader(loader)
|
||||
for dep in self.get_direct_dependencies():
|
||||
dep.set_loader(loader)
|
||||
|
||||
175
lib/ansible/playbook/role/definition.py
Normal file
175
lib/ansible/playbook/role/definition.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# (c) 2014 Michael DeHaan, <michael@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 six import iteritems, string_types
|
||||
|
||||
import os
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.playbook.base import Base
|
||||
from ansible.playbook.become import Become
|
||||
from ansible.playbook.conditional import Conditional
|
||||
from ansible.playbook.taggable import Taggable
|
||||
from ansible.utils.path import unfrackpath
|
||||
|
||||
|
||||
__all__ = ['RoleDefinition']
|
||||
|
||||
|
||||
class RoleDefinition(Base, Become, Conditional, Taggable):
|
||||
|
||||
_role = FieldAttribute(isa='string')
|
||||
|
||||
def __init__(self, role_basedir=None):
|
||||
self._role_path = None
|
||||
self._role_basedir = role_basedir
|
||||
self._role_params = dict()
|
||||
super(RoleDefinition, self).__init__()
|
||||
|
||||
#def __repr__(self):
|
||||
# return 'ROLEDEF: ' + self._attributes.get('role', '<no name set>')
|
||||
|
||||
@staticmethod
|
||||
def load(data, variable_manager=None, loader=None):
|
||||
raise AnsibleError("not implemented")
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
|
||||
assert isinstance(ds, dict) or isinstance(ds, string_types)
|
||||
|
||||
if isinstance(ds, dict):
|
||||
ds = super(RoleDefinition, self).preprocess_data(ds)
|
||||
|
||||
# we create a new data structure here, using the same
|
||||
# object used internally by the YAML parsing code so we
|
||||
# can preserve file:line:column information if it exists
|
||||
new_ds = AnsibleMapping()
|
||||
if isinstance(ds, AnsibleBaseYAMLObject):
|
||||
new_ds.ansible_pos = ds.ansible_pos
|
||||
|
||||
# first we pull the role name out of the data structure,
|
||||
# and then use that to determine the role path (which may
|
||||
# result in a new role name, if it was a file path)
|
||||
role_name = self._load_role_name(ds)
|
||||
(role_name, role_path) = self._load_role_path(role_name)
|
||||
|
||||
# next, we split the role params out from the valid role
|
||||
# attributes and update the new datastructure with that
|
||||
# result and the role name
|
||||
if isinstance(ds, dict):
|
||||
(new_role_def, role_params) = self._split_role_params(ds)
|
||||
new_ds.update(new_role_def)
|
||||
self._role_params = role_params
|
||||
|
||||
# set the role name in the new ds
|
||||
new_ds['role'] = role_name
|
||||
|
||||
# we store the role path internally
|
||||
self._role_path = role_path
|
||||
|
||||
# save the original ds for use later
|
||||
self._ds = ds
|
||||
|
||||
# and return the cleaned-up data structure
|
||||
return new_ds
|
||||
|
||||
def _load_role_name(self, ds):
|
||||
'''
|
||||
Returns the role name (either the role: or name: field) from
|
||||
the role definition, or (when the role definition is a simple
|
||||
string), just that string
|
||||
'''
|
||||
|
||||
if isinstance(ds, string_types):
|
||||
return ds
|
||||
|
||||
role_name = ds.get('role', ds.get('name'))
|
||||
if not role_name:
|
||||
raise AnsibleError('role definitions must contain a role name', obj=ds)
|
||||
|
||||
return role_name
|
||||
|
||||
def _load_role_path(self, role_name):
|
||||
'''
|
||||
the 'role', as specified in the ds (or as a bare string), can either
|
||||
be a simple name or a full path. If it is a full path, we use the
|
||||
basename as the role name, otherwise we take the name as-given and
|
||||
append it to the default role path
|
||||
'''
|
||||
|
||||
role_path = unfrackpath(role_name)
|
||||
|
||||
if self._loader.path_exists(role_path):
|
||||
role_name = os.path.basename(role_name)
|
||||
return (role_name, role_path)
|
||||
else:
|
||||
# we always start the search for roles in the base directory of the playbook
|
||||
role_search_paths = [os.path.join(self._loader.get_basedir(), 'roles'), './roles', './']
|
||||
|
||||
# also search in the configured roles path
|
||||
if C.DEFAULT_ROLES_PATH:
|
||||
configured_paths = C.DEFAULT_ROLES_PATH.split(os.pathsep)
|
||||
role_search_paths.extend(configured_paths)
|
||||
|
||||
# finally, append the roles basedir, if it was set, so we can
|
||||
# search relative to that directory for dependent roles
|
||||
if self._role_basedir:
|
||||
role_search_paths.append(self._role_basedir)
|
||||
|
||||
# now iterate through the possible paths and return the first one we find
|
||||
for path in role_search_paths:
|
||||
role_path = unfrackpath(os.path.join(path, role_name))
|
||||
if self._loader.path_exists(role_path):
|
||||
return (role_name, role_path)
|
||||
|
||||
# FIXME: make the parser smart about list/string entries in
|
||||
# the yaml so the error line/file can be reported here
|
||||
|
||||
raise AnsibleError("the role '%s' was not found" % role_name)
|
||||
|
||||
def _split_role_params(self, ds):
|
||||
'''
|
||||
Splits any random role params off from the role spec and store
|
||||
them in a dictionary of params for parsing later
|
||||
'''
|
||||
|
||||
role_def = dict()
|
||||
role_params = dict()
|
||||
for (key, value) in iteritems(ds):
|
||||
# use the list of FieldAttribute values to determine what is and is not
|
||||
# an extra parameter for this role (or sub-class of this role)
|
||||
if key not in [attr_name for (attr_name, attr_value) in self._get_base_attributes().iteritems()]:
|
||||
# this key does not match a field attribute, so it must be a role param
|
||||
role_params[key] = value
|
||||
else:
|
||||
# this is a field attribute, so copy it over directly
|
||||
role_def[key] = value
|
||||
|
||||
return (role_def, role_params)
|
||||
|
||||
def get_role_params(self):
|
||||
return self._role_params.copy()
|
||||
|
||||
def get_role_path(self):
|
||||
return self._role_path
|
||||
49
lib/ansible/playbook/role/include.py
Normal file
49
lib/ansible/playbook/role/include.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# (c) 2014 Michael DeHaan, <michael@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 six import iteritems, string_types
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.playbook.role.definition import RoleDefinition
|
||||
|
||||
|
||||
__all__ = ['RoleInclude']
|
||||
|
||||
|
||||
class RoleInclude(RoleDefinition):
|
||||
|
||||
"""
|
||||
FIXME: docstring
|
||||
"""
|
||||
|
||||
def __init__(self, role_basedir=None):
|
||||
super(RoleInclude, self).__init__(role_basedir=role_basedir)
|
||||
|
||||
@staticmethod
|
||||
def load(data, current_role_path=None, parent_role=None, variable_manager=None, loader=None):
|
||||
assert isinstance(data, string_types) or isinstance(data, dict)
|
||||
|
||||
ri = RoleInclude(role_basedir=current_role_path)
|
||||
return ri.load_data(data, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
94
lib/ansible/playbook/role/metadata.py
Normal file
94
lib/ansible/playbook/role/metadata.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# (c) 2014 Michael DeHaan, <michael@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
|
||||
|
||||
import os
|
||||
|
||||
from six import iteritems, string_types
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.playbook.base import Base
|
||||
from ansible.playbook.helpers import load_list_of_roles
|
||||
from ansible.playbook.role.include import RoleInclude
|
||||
|
||||
|
||||
__all__ = ['RoleMetadata']
|
||||
|
||||
|
||||
class RoleMetadata(Base):
|
||||
'''
|
||||
This class wraps the parsing and validation of the optional metadata
|
||||
within each Role (meta/main.yml).
|
||||
'''
|
||||
|
||||
_allow_duplicates = FieldAttribute(isa='bool', default=False)
|
||||
_dependencies = FieldAttribute(isa='list', default=[])
|
||||
_galaxy_info = FieldAttribute(isa='GalaxyInfo')
|
||||
|
||||
def __init__(self, owner=None):
|
||||
self._owner = owner
|
||||
super(RoleMetadata, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def load(data, owner, variable_manager=None, loader=None):
|
||||
'''
|
||||
Returns a new RoleMetadata object based on the datastructure passed in.
|
||||
'''
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise AnsibleParserError("the 'meta/main.yml' for role %s is not a dictionary" % owner.get_name())
|
||||
|
||||
m = RoleMetadata(owner=owner).load_data(data, variable_manager=variable_manager, loader=loader)
|
||||
return m
|
||||
|
||||
def _load_dependencies(self, attr, ds):
|
||||
'''
|
||||
This is a helper loading function for the dependencies list,
|
||||
which returns a list of RoleInclude objects
|
||||
'''
|
||||
|
||||
if ds is None:
|
||||
ds = []
|
||||
|
||||
current_role_path = None
|
||||
if self._owner:
|
||||
current_role_path = os.path.dirname(self._owner._role_path)
|
||||
|
||||
return load_list_of_roles(ds, current_role_path=current_role_path, variable_manager=self._variable_manager, loader=self._loader)
|
||||
|
||||
def _load_galaxy_info(self, attr, ds):
|
||||
'''
|
||||
This is a helper loading function for the galaxy info entry
|
||||
in the metadata, which returns a GalaxyInfo object rather than
|
||||
a simple dictionary.
|
||||
'''
|
||||
|
||||
return ds
|
||||
|
||||
def serialize(self):
|
||||
return dict(
|
||||
allow_duplicates = self._allow_duplicates,
|
||||
dependencies = self._dependencies,
|
||||
)
|
||||
|
||||
def deserialize(self, data):
|
||||
setattr(self, 'allow_duplicates', data.get('allow_duplicates', False))
|
||||
setattr(self, 'dependencies', data.get('dependencies', []))
|
||||
166
lib/ansible/playbook/role/requirement.py
Normal file
166
lib/ansible/playbook/role/requirement.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# (c) 2014 Michael DeHaan, <michael@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 six import iteritems, string_types
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.playbook.role.definition import RoleDefinition
|
||||
|
||||
__all__ = ['RoleRequirement']
|
||||
|
||||
|
||||
class RoleRequirement(RoleDefinition):
|
||||
|
||||
"""
|
||||
FIXME: document various ways role specs can be specified
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _get_valid_spec_keys(self):
|
||||
return (
|
||||
'name',
|
||||
'role',
|
||||
'scm',
|
||||
'src',
|
||||
'version',
|
||||
)
|
||||
|
||||
def parse(self, ds):
|
||||
'''
|
||||
FIXME: docstring
|
||||
'''
|
||||
|
||||
assert type(ds) == dict or isinstance(ds, string_types)
|
||||
|
||||
role_name = ''
|
||||
role_params = dict()
|
||||
new_ds = dict()
|
||||
|
||||
if isinstance(ds, string_types):
|
||||
role_name = ds
|
||||
else:
|
||||
ds = self._preprocess_role_spec(ds)
|
||||
(new_ds, role_params) = self._split_role_params(ds)
|
||||
|
||||
# pull the role name out of the ds
|
||||
role_name = new_ds.get('role_name')
|
||||
del ds['role_name']
|
||||
|
||||
return (new_ds, role_name, role_params)
|
||||
|
||||
def _preprocess_role_spec(self, ds):
|
||||
if 'role' in ds:
|
||||
# Old style: {role: "galaxy.role,version,name", other_vars: "here" }
|
||||
role_info = self._role_spec_parse(ds['role'])
|
||||
if isinstance(role_info, dict):
|
||||
# Warning: Slight change in behaviour here. name may be being
|
||||
# overloaded. Previously, name was only a parameter to the role.
|
||||
# Now it is both a parameter to the role and the name that
|
||||
# ansible-galaxy will install under on the local system.
|
||||
if 'name' in ds and 'name' in role_info:
|
||||
del role_info['name']
|
||||
ds.update(role_info)
|
||||
else:
|
||||
# New style: { src: 'galaxy.role,version,name', other_vars: "here" }
|
||||
if 'github.com' in ds["src"] and 'http' in ds["src"] and '+' not in ds["src"] and not ds["src"].endswith('.tar.gz'):
|
||||
ds["src"] = "git+" + ds["src"]
|
||||
|
||||
if '+' in ds["src"]:
|
||||
(scm, src) = ds["src"].split('+')
|
||||
ds["scm"] = scm
|
||||
ds["src"] = src
|
||||
|
||||
if 'name' in ds:
|
||||
ds["role"] = ds["name"]
|
||||
del ds["name"]
|
||||
else:
|
||||
ds["role"] = self._repo_url_to_role_name(ds["src"])
|
||||
|
||||
# set some values to a default value, if none were specified
|
||||
ds.setdefault('version', '')
|
||||
ds.setdefault('scm', None)
|
||||
|
||||
return ds
|
||||
|
||||
def _repo_url_to_role_name(self, repo_url):
|
||||
# gets the role name out of a repo like
|
||||
# http://git.example.com/repos/repo.git" => "repo"
|
||||
|
||||
if '://' not in repo_url and '@' not in repo_url:
|
||||
return repo_url
|
||||
trailing_path = repo_url.split('/')[-1]
|
||||
if trailing_path.endswith('.git'):
|
||||
trailing_path = trailing_path[:-4]
|
||||
if trailing_path.endswith('.tar.gz'):
|
||||
trailing_path = trailing_path[:-7]
|
||||
if ',' in trailing_path:
|
||||
trailing_path = trailing_path.split(',')[0]
|
||||
return trailing_path
|
||||
|
||||
def _role_spec_parse(self, role_spec):
|
||||
# takes a repo and a version like
|
||||
# git+http://git.example.com/repos/repo.git,v1.0
|
||||
# and returns a list of properties such as:
|
||||
# {
|
||||
# 'scm': 'git',
|
||||
# 'src': 'http://git.example.com/repos/repo.git',
|
||||
# 'version': 'v1.0',
|
||||
# 'name': 'repo'
|
||||
# }
|
||||
|
||||
default_role_versions = dict(git='master', hg='tip')
|
||||
|
||||
role_spec = role_spec.strip()
|
||||
role_version = ''
|
||||
if role_spec == "" or role_spec.startswith("#"):
|
||||
return (None, None, None, None)
|
||||
|
||||
tokens = [s.strip() for s in role_spec.split(',')]
|
||||
|
||||
# assume https://github.com URLs are git+https:// URLs and not
|
||||
# tarballs unless they end in '.zip'
|
||||
if 'github.com/' in tokens[0] and not tokens[0].startswith("git+") and not tokens[0].endswith('.tar.gz'):
|
||||
tokens[0] = 'git+' + tokens[0]
|
||||
|
||||
if '+' in tokens[0]:
|
||||
(scm, role_url) = tokens[0].split('+')
|
||||
else:
|
||||
scm = None
|
||||
role_url = tokens[0]
|
||||
|
||||
if len(tokens) >= 2:
|
||||
role_version = tokens[1]
|
||||
|
||||
if len(tokens) == 3:
|
||||
role_name = tokens[2]
|
||||
else:
|
||||
role_name = self._repo_url_to_role_name(tokens[0])
|
||||
|
||||
if scm and not role_version:
|
||||
role_version = default_role_versions.get(scm, '')
|
||||
|
||||
return dict(scm=scm, src=role_url, version=role_version, role_name=role_name)
|
||||
|
||||
|
||||
95
lib/ansible/playbook/taggable.py
Normal file
95
lib/ansible/playbook/taggable.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# (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.playbook.attribute import FieldAttribute
|
||||
from ansible.template import Templar
|
||||
|
||||
class Taggable:
|
||||
|
||||
untagged = set(['untagged'])
|
||||
_tags = FieldAttribute(isa='list', default=[])
|
||||
|
||||
def __init__(self):
|
||||
super(Taggable, self).__init__()
|
||||
|
||||
def _load_tags(self, attr, ds):
|
||||
if isinstance(ds, list):
|
||||
return ds
|
||||
elif isinstance(ds, basestring):
|
||||
return [ ds ]
|
||||
else:
|
||||
raise AnsibleError('tags must be specified as a list', obj=ds)
|
||||
|
||||
def _get_attr_tags(self):
|
||||
'''
|
||||
Override for the 'tags' getattr fetcher, used from Base.
|
||||
'''
|
||||
tags = self._attributes['tags']
|
||||
if tags is None:
|
||||
tags = []
|
||||
if hasattr(self, '_get_parent_attribute'):
|
||||
tags = self._get_parent_attribute('tags', extend=True)
|
||||
return tags
|
||||
|
||||
def evaluate_tags(self, only_tags, skip_tags, all_vars):
|
||||
''' this checks if the current item should be executed depending on tag options '''
|
||||
|
||||
should_run = True
|
||||
|
||||
if self.tags:
|
||||
templar = Templar(loader=self._loader, variables=all_vars)
|
||||
tags = templar.template(self.tags)
|
||||
|
||||
if not isinstance(tags, list):
|
||||
if tags.find(',') != -1:
|
||||
tags = set(tags.split(','))
|
||||
else:
|
||||
tags = set([tags])
|
||||
else:
|
||||
tags = set(tags)
|
||||
else:
|
||||
# this makes intersection work for untagged
|
||||
tags = self.__class__.untagged
|
||||
|
||||
if only_tags:
|
||||
|
||||
should_run = False
|
||||
|
||||
if 'always' in tags or 'all' in only_tags:
|
||||
should_run = True
|
||||
elif tags.intersection(only_tags):
|
||||
should_run = True
|
||||
elif 'tagged' in only_tags and tags != self.__class__.untagged:
|
||||
should_run = True
|
||||
|
||||
if should_run and skip_tags:
|
||||
|
||||
# Check for tags that we need to skip
|
||||
if 'all' in skip_tags:
|
||||
if 'always' not in tags or 'always' in skip_tags:
|
||||
should_run = False
|
||||
elif tags.intersection(skip_tags):
|
||||
should_run = False
|
||||
elif 'tagged' in skip_tags and tags != self.__class__.untagged:
|
||||
should_run = False
|
||||
|
||||
return should_run
|
||||
@@ -15,332 +15,296 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
from ansible.module_utils.splitter import split_args
|
||||
import os
|
||||
import ansible.utils.template as template
|
||||
import sys
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
class Task(object):
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
_t_common = [
|
||||
'action', 'always_run', 'any_errors_fatal', 'args', 'become', 'become_method', 'become_pass',
|
||||
'become_user', 'changed_when', 'delay', 'delegate_to', 'environment', 'failed_when',
|
||||
'first_available_file', 'ignore_errors', 'local_action', 'meta', 'name', 'no_log',
|
||||
'notify', 'register', 'remote_user', 'retries', 'run_once', 'su', 'su_pass', 'su_user',
|
||||
'sudo', 'sudo_pass', 'sudo_user', 'tags', 'transport', 'until', 'when',
|
||||
]
|
||||
from ansible.parsing.mod_args import ModuleArgsParser
|
||||
from ansible.parsing.splitter import parse_kv
|
||||
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
|
||||
|
||||
__slots__ = [
|
||||
'async_poll_interval', 'async_seconds', 'default_vars', 'first_available_file',
|
||||
'items_lookup_plugin', 'items_lookup_terms', 'module_args', 'module_name', 'module_vars',
|
||||
'notified_by', 'play', 'play_file_vars', 'play_vars', 'role_name', 'role_params', 'role_vars',
|
||||
] + _t_common
|
||||
from ansible.plugins import module_loader, lookup_loader
|
||||
|
||||
# to prevent typos and such
|
||||
VALID_KEYS = frozenset([
|
||||
'async', 'connection', 'include', 'poll',
|
||||
] + _t_common)
|
||||
from ansible.playbook.attribute import Attribute, FieldAttribute
|
||||
from ansible.playbook.base import Base
|
||||
from ansible.playbook.become import Become
|
||||
from ansible.playbook.block import Block
|
||||
from ansible.playbook.conditional import Conditional
|
||||
from ansible.playbook.role import Role
|
||||
from ansible.playbook.taggable import Taggable
|
||||
|
||||
def __init__(self, play, ds, module_vars=None, play_vars=None, play_file_vars=None, role_vars=None, role_params=None, default_vars=None, additional_conditions=None, role_name=None):
|
||||
''' constructor loads from a task or handler datastructure '''
|
||||
__all__ = ['Task']
|
||||
|
||||
# meta directives are used to tell things like ansible/playbook to run
|
||||
# operations like handler execution. Meta tasks are not executed
|
||||
# normally.
|
||||
if 'meta' in ds:
|
||||
self.meta = ds['meta']
|
||||
self.tags = []
|
||||
self.module_vars = module_vars
|
||||
self.role_name = role_name
|
||||
return
|
||||
else:
|
||||
self.meta = None
|
||||
class Task(Base, Conditional, Taggable, Become):
|
||||
|
||||
"""
|
||||
A task is a language feature that represents a call to a module, with given arguments and other parameters.
|
||||
A handler is a subclass of a task.
|
||||
|
||||
library = os.path.join(play.basedir, 'library')
|
||||
if os.path.exists(library):
|
||||
utils.plugins.module_finder.add_directory(library)
|
||||
Usage:
|
||||
|
||||
for x in ds.keys():
|
||||
Task.load(datastructure) -> Task
|
||||
Task.something(...)
|
||||
"""
|
||||
|
||||
# code to allow for saying "modulename: args" versus "action: modulename args"
|
||||
if x in utils.plugins.module_finder:
|
||||
# =================================================================================
|
||||
# ATTRIBUTES
|
||||
# load_<attribute_name> and
|
||||
# validate_<attribute_name>
|
||||
# will be used if defined
|
||||
# might be possible to define others
|
||||
|
||||
if 'action' in ds:
|
||||
raise errors.AnsibleError("multiple actions specified in task: '%s' and '%s'" % (x, ds.get('name', ds['action'])))
|
||||
if isinstance(ds[x], dict):
|
||||
if 'args' in ds:
|
||||
raise errors.AnsibleError("can't combine args: and a dict for %s: in task %s" % (x, ds.get('name', "%s: %s" % (x, ds[x]))))
|
||||
ds['args'] = ds[x]
|
||||
ds[x] = ''
|
||||
elif ds[x] is None:
|
||||
ds[x] = ''
|
||||
if not isinstance(ds[x], basestring):
|
||||
raise errors.AnsibleError("action specified for task %s has invalid type %s" % (ds.get('name', "%s: %s" % (x, ds[x])), type(ds[x])))
|
||||
ds['action'] = x + " " + ds[x]
|
||||
ds.pop(x)
|
||||
_args = FieldAttribute(isa='dict', default=dict())
|
||||
_action = FieldAttribute(isa='string')
|
||||
|
||||
# code to allow "with_glob" and to reference a lookup plugin named glob
|
||||
elif x.startswith("with_"):
|
||||
if isinstance(ds[x], basestring):
|
||||
param = ds[x].strip()
|
||||
_always_run = FieldAttribute(isa='bool')
|
||||
_any_errors_fatal = FieldAttribute(isa='bool')
|
||||
_async = FieldAttribute(isa='int', default=0)
|
||||
_changed_when = FieldAttribute(isa='string')
|
||||
_delay = FieldAttribute(isa='int', default=5)
|
||||
_delegate_to = FieldAttribute(isa='string')
|
||||
_failed_when = FieldAttribute(isa='string')
|
||||
_first_available_file = FieldAttribute(isa='list')
|
||||
_ignore_errors = FieldAttribute(isa='bool')
|
||||
|
||||
plugin_name = x.replace("with_","")
|
||||
if plugin_name in utils.plugins.lookup_loader:
|
||||
ds['items_lookup_plugin'] = plugin_name
|
||||
ds['items_lookup_terms'] = ds[x]
|
||||
ds.pop(x)
|
||||
else:
|
||||
raise errors.AnsibleError("cannot find lookup plugin named %s for usage in with_%s" % (plugin_name, plugin_name))
|
||||
_loop = FieldAttribute(isa='string', private=True)
|
||||
_loop_args = FieldAttribute(isa='list', private=True)
|
||||
_local_action = FieldAttribute(isa='string')
|
||||
|
||||
elif x in [ 'changed_when', 'failed_when', 'when']:
|
||||
if isinstance(ds[x], basestring):
|
||||
param = ds[x].strip()
|
||||
# Only a variable, no logic
|
||||
if (param.startswith('{{') and
|
||||
param.find('}}') == len(ds[x]) - 2 and
|
||||
param.find('|') == -1):
|
||||
utils.warning("It is unnecessary to use '{{' in conditionals, leave variables in loop expressions bare.")
|
||||
elif x.startswith("when_"):
|
||||
utils.deprecated("The 'when_' conditional has been removed. Switch to using the regular unified 'when' statements as described on docs.ansible.com.","1.5", removed=True)
|
||||
# FIXME: this should not be a Task
|
||||
_meta = FieldAttribute(isa='string')
|
||||
|
||||
if 'when' in ds:
|
||||
raise errors.AnsibleError("multiple when_* statements specified in task %s" % (ds.get('name', ds['action'])))
|
||||
when_name = x.replace("when_","")
|
||||
ds['when'] = "%s %s" % (when_name, ds[x])
|
||||
ds.pop(x)
|
||||
elif not x in Task.VALID_KEYS:
|
||||
raise errors.AnsibleError("%s is not a legal parameter in an Ansible task or handler" % x)
|
||||
_name = FieldAttribute(isa='string', default='')
|
||||
|
||||
self.module_vars = module_vars
|
||||
self.play_vars = play_vars
|
||||
self.play_file_vars = play_file_vars
|
||||
self.role_vars = role_vars
|
||||
self.role_params = role_params
|
||||
self.default_vars = default_vars
|
||||
self.play = play
|
||||
_notify = FieldAttribute(isa='list')
|
||||
_poll = FieldAttribute(isa='int')
|
||||
_register = FieldAttribute(isa='string')
|
||||
_retries = FieldAttribute(isa='int', default=1)
|
||||
_run_once = FieldAttribute(isa='bool')
|
||||
_until = FieldAttribute(isa='list') # ?
|
||||
|
||||
# load various attributes
|
||||
self.name = ds.get('name', None)
|
||||
self.tags = [ 'untagged' ]
|
||||
self.register = ds.get('register', None)
|
||||
self.environment = ds.get('environment', play.environment)
|
||||
self.role_name = role_name
|
||||
self.no_log = utils.boolean(ds.get('no_log', "false")) or self.play.no_log
|
||||
self.run_once = utils.boolean(ds.get('run_once', 'false'))
|
||||
def __init__(self, block=None, role=None, task_include=None):
|
||||
''' constructors a task, without the Task.load classmethod, it will be pretty blank '''
|
||||
|
||||
#Code to allow do until feature in a Task
|
||||
if 'until' in ds:
|
||||
if not ds.get('register'):
|
||||
raise errors.AnsibleError("register keyword is mandatory when using do until feature")
|
||||
self.module_vars['delay'] = ds.get('delay', 5)
|
||||
self.module_vars['retries'] = ds.get('retries', 3)
|
||||
self.module_vars['register'] = ds.get('register', None)
|
||||
self.until = ds.get('until')
|
||||
self.module_vars['until'] = self.until
|
||||
self._block = block
|
||||
self._role = role
|
||||
self._task_include = task_include
|
||||
|
||||
# rather than simple key=value args on the options line, these represent structured data and the values
|
||||
# can be hashes and lists, not just scalars
|
||||
self.args = ds.get('args', {})
|
||||
super(Task, self).__init__()
|
||||
|
||||
# get remote_user for task, then play, then playbook
|
||||
if ds.get('remote_user') is not None:
|
||||
self.remote_user = ds.get('remote_user')
|
||||
elif ds.get('remote_user', play.remote_user) is not None:
|
||||
self.remote_user = ds.get('remote_user', play.remote_user)
|
||||
else:
|
||||
self.remote_user = ds.get('remote_user', play.playbook.remote_user)
|
||||
def get_name(self):
|
||||
''' return the name of the task '''
|
||||
|
||||
# Fail out if user specifies privilege escalation params in conflict
|
||||
if (ds.get('become') or ds.get('become_user') or ds.get('become_pass')) and (ds.get('sudo') or ds.get('sudo_user') or ds.get('sudo_pass')):
|
||||
raise errors.AnsibleError('incompatible parameters ("become", "become_user", "become_pass") and sudo params "sudo", "sudo_user", "sudo_pass" in task: %s' % self.name)
|
||||
if self._role and self.name:
|
||||
return "%s : %s" % (self._role.get_name(), self.name)
|
||||
elif self.name:
|
||||
return self.name
|
||||
else:
|
||||
flattened_args = self._merge_kv(self.args)
|
||||
if self._role:
|
||||
return "%s : %s %s" % (self._role.get_name(), self.action, flattened_args)
|
||||
else:
|
||||
return "%s %s" % (self.action, flattened_args)
|
||||
|
||||
if (ds.get('become') or ds.get('become_user') or ds.get('become_pass')) and (ds.get('su') or ds.get('su_user') or ds.get('su_pass')):
|
||||
raise errors.AnsibleError('incompatible parameters ("become", "become_user", "become_pass") and su params "su", "su_user", "sudo_pass" in task: %s' % self.name)
|
||||
def _merge_kv(self, ds):
|
||||
if ds is None:
|
||||
return ""
|
||||
elif isinstance(ds, basestring):
|
||||
return ds
|
||||
elif isinstance(ds, dict):
|
||||
buf = ""
|
||||
for (k,v) in ds.iteritems():
|
||||
if k.startswith('_'):
|
||||
continue
|
||||
buf = buf + "%s=%s " % (k,v)
|
||||
buf = buf.strip()
|
||||
return buf
|
||||
|
||||
if (ds.get('sudo') or ds.get('sudo_user') or ds.get('sudo_pass')) and (ds.get('su') or ds.get('su_user') or ds.get('su_pass')):
|
||||
raise errors.AnsibleError('incompatible parameters ("su", "su_user", "su_pass") and sudo params "sudo", "sudo_user", "sudo_pass" in task: %s' % self.name)
|
||||
@staticmethod
|
||||
def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None):
|
||||
t = Task(block=block, role=role, task_include=task_include)
|
||||
return t.load_data(data, variable_manager=variable_manager, loader=loader)
|
||||
|
||||
self.become = utils.boolean(ds.get('become', play.become))
|
||||
self.become_method = ds.get('become_method', play.become_method)
|
||||
self.become_user = ds.get('become_user', play.become_user)
|
||||
self.become_pass = ds.get('become_pass', play.playbook.become_pass)
|
||||
def __repr__(self):
|
||||
''' returns a human readable representation of the task '''
|
||||
return "TASK: %s" % self.get_name()
|
||||
|
||||
# set only if passed in current task data
|
||||
if 'sudo' in ds or 'sudo_user' in ds:
|
||||
self.become_method='sudo'
|
||||
def _preprocess_loop(self, ds, new_ds, k, v):
|
||||
''' take a lookup plugin name and store it correctly '''
|
||||
|
||||
if 'sudo' in ds:
|
||||
self.become=ds['sudo']
|
||||
del ds['sudo']
|
||||
loop_name = k.replace("with_", "")
|
||||
if new_ds.get('loop') is not None:
|
||||
raise AnsibleError("duplicate loop in task: %s" % loop_name)
|
||||
new_ds['loop'] = loop_name
|
||||
new_ds['loop_args'] = v
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
'''
|
||||
tasks are especially complex arguments so need pre-processing.
|
||||
keep it short.
|
||||
'''
|
||||
|
||||
assert isinstance(ds, dict)
|
||||
|
||||
# the new, cleaned datastructure, which will have legacy
|
||||
# items reduced to a standard structure suitable for the
|
||||
# attributes of the task class
|
||||
new_ds = AnsibleMapping()
|
||||
if isinstance(ds, AnsibleBaseYAMLObject):
|
||||
new_ds.ansible_pos = ds.ansible_pos
|
||||
|
||||
# use the args parsing class to determine the action, args,
|
||||
# and the delegate_to value from the various possible forms
|
||||
# supported as legacy
|
||||
args_parser = ModuleArgsParser(task_ds=ds)
|
||||
(action, args, delegate_to) = args_parser.parse()
|
||||
|
||||
new_ds['action'] = action
|
||||
new_ds['args'] = args
|
||||
new_ds['delegate_to'] = delegate_to
|
||||
|
||||
for (k,v) in ds.iteritems():
|
||||
if k in ('action', 'local_action', 'args', 'delegate_to') or k == action or k == 'shell':
|
||||
# we don't want to re-assign these values, which were
|
||||
# determined by the ModuleArgsParser() above
|
||||
continue
|
||||
elif k.replace("with_", "") in lookup_loader:
|
||||
self._preprocess_loop(ds, new_ds, k, v)
|
||||
else:
|
||||
self.become=True
|
||||
if 'sudo_user' in ds:
|
||||
self.become_user = ds['sudo_user']
|
||||
del ds['sudo_user']
|
||||
if 'sudo_pass' in ds:
|
||||
self.become_pass = ds['sudo_pass']
|
||||
del ds['sudo_pass']
|
||||
new_ds[k] = v
|
||||
|
||||
elif 'su' in ds or 'su_user' in ds:
|
||||
self.become_method='su'
|
||||
return super(Task, self).preprocess_data(new_ds)
|
||||
|
||||
if 'su' in ds:
|
||||
self.become=ds['su']
|
||||
def post_validate(self, templar):
|
||||
'''
|
||||
Override of base class post_validate, to also do final validation on
|
||||
the block and task include (if any) to which this task belongs.
|
||||
'''
|
||||
|
||||
if self._block:
|
||||
self._block.post_validate(templar)
|
||||
if self._task_include:
|
||||
self._task_include.post_validate(templar)
|
||||
|
||||
super(Task, self).post_validate(templar)
|
||||
|
||||
def get_vars(self):
|
||||
all_vars = self.vars.copy()
|
||||
if self._block:
|
||||
all_vars.update(self._block.get_vars())
|
||||
if self._task_include:
|
||||
all_vars.update(self._task_include.get_vars())
|
||||
|
||||
all_vars.update(self.serialize())
|
||||
|
||||
if 'tags' in all_vars:
|
||||
del all_vars['tags']
|
||||
if 'when' in all_vars:
|
||||
del all_vars['when']
|
||||
return all_vars
|
||||
|
||||
def copy(self, exclude_block=False):
|
||||
new_me = super(Task, self).copy()
|
||||
|
||||
new_me._block = None
|
||||
if self._block and not exclude_block:
|
||||
new_me._block = self._block.copy()
|
||||
|
||||
new_me._role = None
|
||||
if self._role:
|
||||
new_me._role = self._role
|
||||
|
||||
new_me._task_include = None
|
||||
if self._task_include:
|
||||
new_me._task_include = self._task_include.copy()
|
||||
|
||||
return new_me
|
||||
|
||||
def serialize(self):
|
||||
data = super(Task, self).serialize()
|
||||
|
||||
if self._block:
|
||||
data['block'] = self._block.serialize()
|
||||
|
||||
if self._role:
|
||||
data['role'] = self._role.serialize()
|
||||
|
||||
if self._task_include:
|
||||
data['task_include'] = self._task_include.serialize()
|
||||
|
||||
return data
|
||||
|
||||
def deserialize(self, data):
|
||||
|
||||
# import is here to avoid import loops
|
||||
#from ansible.playbook.task_include import TaskInclude
|
||||
|
||||
block_data = data.get('block')
|
||||
|
||||
if block_data:
|
||||
b = Block()
|
||||
b.deserialize(block_data)
|
||||
self._block = b
|
||||
del data['block']
|
||||
|
||||
role_data = data.get('role')
|
||||
if role_data:
|
||||
r = Role()
|
||||
r.deserialize(role_data)
|
||||
self._role = r
|
||||
del data['role']
|
||||
|
||||
ti_data = data.get('task_include')
|
||||
if ti_data:
|
||||
#ti = TaskInclude()
|
||||
ti = Task()
|
||||
ti.deserialize(ti_data)
|
||||
self._task_include = ti
|
||||
del data['task_include']
|
||||
|
||||
super(Task, self).deserialize(data)
|
||||
|
||||
def evaluate_conditional(self, templar, all_vars):
|
||||
if self._block is not None:
|
||||
if not self._block.evaluate_conditional(templar, all_vars):
|
||||
return False
|
||||
if self._task_include is not None:
|
||||
if not self._task_include.evaluate_conditional(templar, all_vars):
|
||||
return False
|
||||
return super(Task, self).evaluate_conditional(templar, all_vars)
|
||||
|
||||
def set_loader(self, loader):
|
||||
'''
|
||||
Sets the loader on this object and recursively on parent, child objects.
|
||||
This is used primarily after the Task has been serialized/deserialized, which
|
||||
does not preserve the loader.
|
||||
'''
|
||||
|
||||
self._loader = loader
|
||||
|
||||
if self._block:
|
||||
self._block.set_loader(loader)
|
||||
if self._task_include:
|
||||
self._task_include.set_loader(loader)
|
||||
|
||||
def _get_parent_attribute(self, attr, extend=False):
|
||||
'''
|
||||
Generic logic to get the attribute or parent attribute for a task value.
|
||||
'''
|
||||
value = self._attributes[attr]
|
||||
if self._block and (not value or extend):
|
||||
parent_value = getattr(self._block, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, parent_value)
|
||||
else:
|
||||
self.become=True
|
||||
del ds['su']
|
||||
if 'su_user' in ds:
|
||||
self.become_user = ds['su_user']
|
||||
del ds['su_user']
|
||||
if 'su_pass' in ds:
|
||||
self.become_pass = ds['su_pass']
|
||||
del ds['su_pass']
|
||||
|
||||
# Both are defined
|
||||
if ('action' in ds) and ('local_action' in ds):
|
||||
raise errors.AnsibleError("the 'action' and 'local_action' attributes can not be used together")
|
||||
# Both are NOT defined
|
||||
elif (not 'action' in ds) and (not 'local_action' in ds):
|
||||
raise errors.AnsibleError("'action' or 'local_action' attribute missing in task \"%s\"" % ds.get('name', '<Unnamed>'))
|
||||
# Only one of them is defined
|
||||
elif 'local_action' in ds:
|
||||
self.action = ds.get('local_action', '')
|
||||
self.delegate_to = '127.0.0.1'
|
||||
else:
|
||||
self.action = ds.get('action', '')
|
||||
self.delegate_to = ds.get('delegate_to', None)
|
||||
self.transport = ds.get('connection', ds.get('transport', play.transport))
|
||||
|
||||
if isinstance(self.action, dict):
|
||||
if 'module' not in self.action:
|
||||
raise errors.AnsibleError("'module' attribute missing from action in task \"%s\"" % ds.get('name', '%s' % self.action))
|
||||
if self.args:
|
||||
raise errors.AnsibleError("'args' cannot be combined with dict 'action' in task \"%s\"" % ds.get('name', '%s' % self.action))
|
||||
self.args = self.action
|
||||
self.action = self.args.pop('module')
|
||||
|
||||
# delegate_to can use variables
|
||||
if not (self.delegate_to is None):
|
||||
# delegate_to: localhost should use local transport
|
||||
if self.delegate_to in ['127.0.0.1', 'localhost']:
|
||||
self.transport = 'local'
|
||||
|
||||
# notified by is used by Playbook code to flag which hosts
|
||||
# need to run a notifier
|
||||
self.notified_by = []
|
||||
|
||||
# if no name is specified, use the action line as the name
|
||||
if self.name is None:
|
||||
self.name = self.action
|
||||
|
||||
# load various attributes
|
||||
self.when = ds.get('when', None)
|
||||
self.changed_when = ds.get('changed_when', None)
|
||||
self.failed_when = ds.get('failed_when', None)
|
||||
|
||||
# combine the default and module vars here for use in templating
|
||||
all_vars = self.default_vars.copy()
|
||||
all_vars = utils.combine_vars(all_vars, self.play_vars)
|
||||
all_vars = utils.combine_vars(all_vars, self.play_file_vars)
|
||||
all_vars = utils.combine_vars(all_vars, self.role_vars)
|
||||
all_vars = utils.combine_vars(all_vars, self.module_vars)
|
||||
all_vars = utils.combine_vars(all_vars, self.role_params)
|
||||
|
||||
self.async_seconds = ds.get('async', 0) # not async by default
|
||||
self.async_seconds = template.template_from_string(play.basedir, self.async_seconds, all_vars)
|
||||
self.async_seconds = int(self.async_seconds)
|
||||
self.async_poll_interval = ds.get('poll', 10) # default poll = 10 seconds
|
||||
self.async_poll_interval = template.template_from_string(play.basedir, self.async_poll_interval, all_vars)
|
||||
self.async_poll_interval = int(self.async_poll_interval)
|
||||
self.notify = ds.get('notify', [])
|
||||
self.first_available_file = ds.get('first_available_file', None)
|
||||
|
||||
self.items_lookup_plugin = ds.get('items_lookup_plugin', None)
|
||||
self.items_lookup_terms = ds.get('items_lookup_terms', None)
|
||||
|
||||
|
||||
self.ignore_errors = ds.get('ignore_errors', False)
|
||||
self.any_errors_fatal = ds.get('any_errors_fatal', play.any_errors_fatal)
|
||||
|
||||
self.always_run = ds.get('always_run', False)
|
||||
|
||||
# action should be a string
|
||||
if not isinstance(self.action, basestring):
|
||||
raise errors.AnsibleError("action is of type '%s' and not a string in task. name: %s" % (type(self.action).__name__, self.name))
|
||||
|
||||
# notify can be a string or a list, store as a list
|
||||
if isinstance(self.notify, basestring):
|
||||
self.notify = [ self.notify ]
|
||||
|
||||
# split the action line into a module name + arguments
|
||||
try:
|
||||
tokens = split_args(self.action)
|
||||
except Exception, e:
|
||||
if "unbalanced" in str(e):
|
||||
raise errors.AnsibleError("There was an error while parsing the task %s.\n" % repr(self.action) + \
|
||||
"Make sure quotes are matched or escaped properly")
|
||||
value = parent_value
|
||||
if self._task_include and (not value or extend):
|
||||
parent_value = getattr(self._task_include, attr)
|
||||
if extend:
|
||||
value = self._extend_value(value, parent_value)
|
||||
else:
|
||||
raise
|
||||
if len(tokens) < 1:
|
||||
raise errors.AnsibleError("invalid/missing action in task. name: %s" % self.name)
|
||||
self.module_name = tokens[0]
|
||||
self.module_args = ''
|
||||
if len(tokens) > 1:
|
||||
self.module_args = " ".join(tokens[1:])
|
||||
value = parent_value
|
||||
return value
|
||||
|
||||
import_tags = self.module_vars.get('tags',[])
|
||||
if type(import_tags) in [int,float]:
|
||||
import_tags = str(import_tags)
|
||||
elif type(import_tags) in [str,unicode]:
|
||||
# allow the user to list comma delimited tags
|
||||
import_tags = import_tags.split(",")
|
||||
|
||||
# handle mutually incompatible options
|
||||
incompatibles = [ x for x in [ self.first_available_file, self.items_lookup_plugin ] if x is not None ]
|
||||
if len(incompatibles) > 1:
|
||||
raise errors.AnsibleError("with_(plugin), and first_available_file are mutually incompatible in a single task")
|
||||
|
||||
# make first_available_file accessible to Runner code
|
||||
if self.first_available_file:
|
||||
self.module_vars['first_available_file'] = self.first_available_file
|
||||
# make sure that the 'item' variable is set when using
|
||||
# first_available_file (issue #8220)
|
||||
if 'item' not in self.module_vars:
|
||||
self.module_vars['item'] = ''
|
||||
|
||||
if self.items_lookup_plugin is not None:
|
||||
self.module_vars['items_lookup_plugin'] = self.items_lookup_plugin
|
||||
self.module_vars['items_lookup_terms'] = self.items_lookup_terms
|
||||
|
||||
# allow runner to see delegate_to option
|
||||
self.module_vars['delegate_to'] = self.delegate_to
|
||||
|
||||
# make some task attributes accessible to Runner code
|
||||
self.module_vars['ignore_errors'] = self.ignore_errors
|
||||
self.module_vars['register'] = self.register
|
||||
self.module_vars['changed_when'] = self.changed_when
|
||||
self.module_vars['failed_when'] = self.failed_when
|
||||
self.module_vars['always_run'] = self.always_run
|
||||
|
||||
# tags allow certain parts of a playbook to be run without running the whole playbook
|
||||
apply_tags = ds.get('tags', None)
|
||||
if apply_tags is not None:
|
||||
if type(apply_tags) in [ str, unicode ]:
|
||||
self.tags.append(apply_tags)
|
||||
elif type(apply_tags) in [ int, float ]:
|
||||
self.tags.append(str(apply_tags))
|
||||
elif type(apply_tags) == list:
|
||||
self.tags.extend(apply_tags)
|
||||
self.tags.extend(import_tags)
|
||||
|
||||
if len(self.tags) > 1:
|
||||
self.tags.remove('untagged')
|
||||
|
||||
if additional_conditions:
|
||||
new_conditions = additional_conditions[:]
|
||||
if self.when:
|
||||
new_conditions.append(self.when)
|
||||
self.when = new_conditions
|
||||
|
||||
21
lib/ansible/playbook/vars.py
Normal file
21
lib/ansible/playbook/vars.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
|
||||
|
||||
21
lib/ansible/playbook/vars_file.py
Normal file
21
lib/ansible/playbook/vars_file.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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# (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
|
||||
#
|
||||
@@ -15,12 +16,19 @@
|
||||
# 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
|
||||
import glob
|
||||
import imp
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.utils.display import Display
|
||||
from ansible import errors
|
||||
|
||||
MODULE_CACHE = {}
|
||||
@@ -34,7 +42,10 @@ def push_basedir(basedir):
|
||||
if basedir not in _basedirs:
|
||||
_basedirs.insert(0, basedir)
|
||||
|
||||
class PluginLoader(object):
|
||||
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.
|
||||
@@ -44,9 +55,10 @@ class PluginLoader(object):
|
||||
The first match is used.
|
||||
'''
|
||||
|
||||
def __init__(self, class_name, package, config, subdir, aliases={}):
|
||||
def __init__(self, class_name, package, config, subdir, aliases={}, required_base_class=None):
|
||||
|
||||
self.class_name = class_name
|
||||
self.base_class = required_base_class
|
||||
self.package = package
|
||||
self.config = config
|
||||
self.subdir = subdir
|
||||
@@ -66,6 +78,43 @@ class PluginLoader(object):
|
||||
self._extra_dirs = []
|
||||
self._searched_paths = set()
|
||||
|
||||
def __setstate__(self, data):
|
||||
'''
|
||||
Deserializer.
|
||||
'''
|
||||
|
||||
class_name = data.get('class_name')
|
||||
package = data.get('package')
|
||||
config = data.get('config')
|
||||
subdir = data.get('subdir')
|
||||
aliases = data.get('aliases')
|
||||
base_class = data.get('base_class')
|
||||
|
||||
PATH_CACHE[class_name] = data.get('PATH_CACHE')
|
||||
PLUGIN_PATH_CACHE[class_name] = data.get('PLUGIN_PATH_CACHE')
|
||||
|
||||
self.__init__(class_name, package, config, subdir, aliases, base_class)
|
||||
self._extra_dirs = data.get('_extra_dirs', [])
|
||||
self._searched_paths = data.get('_searched_paths', set())
|
||||
|
||||
def __getstate__(self):
|
||||
'''
|
||||
Serializer.
|
||||
'''
|
||||
|
||||
return dict(
|
||||
class_name = self.class_name,
|
||||
base_class = self.base_class,
|
||||
package = self.package,
|
||||
config = self.config,
|
||||
subdir = self.subdir,
|
||||
aliases = self.aliases,
|
||||
_extra_dirs = self._extra_dirs,
|
||||
_searched_paths = self._searched_paths,
|
||||
PATH_CACHE = PATH_CACHE[self.class_name],
|
||||
PLUGIN_PATH_CACHE = PLUGIN_PATH_CACHE[self.class_name],
|
||||
)
|
||||
|
||||
def print_paths(self):
|
||||
''' Returns a string suitable for printing of the search path '''
|
||||
|
||||
@@ -108,7 +157,6 @@ class PluginLoader(object):
|
||||
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
|
||||
@@ -173,7 +221,11 @@ class PluginLoader(object):
|
||||
found = None
|
||||
for path in [p for p in self._get_paths() if p not in self._searched_paths]:
|
||||
if os.path.isdir(path):
|
||||
full_paths = (os.path.join(path, f) for f in os.listdir(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):
|
||||
@@ -214,9 +266,18 @@ class PluginLoader(object):
|
||||
path = self.find_plugin(name)
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
if self.base_class and self.base_class not in [base.__name__ for base in obj.__class__.__bases__]:
|
||||
return None
|
||||
|
||||
return obj
|
||||
|
||||
def all(self, *args, **kwargs):
|
||||
''' instantiates all plugins with the same arguments '''
|
||||
@@ -228,72 +289,87 @@ class PluginLoader(object):
|
||||
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)
|
||||
yield getattr(self._module_cache[path], self.class_name)(*args, **kwargs)
|
||||
|
||||
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)
|
||||
|
||||
if self.base_class and self.base_class not in [base.__name__ for base in obj.__class__.__bases__]:
|
||||
continue
|
||||
|
||||
# set extra info on the module, in case we want it later
|
||||
setattr(obj, '_original_path', path)
|
||||
yield obj
|
||||
|
||||
action_loader = PluginLoader(
|
||||
'ActionModule',
|
||||
'ansible.runner.action_plugins',
|
||||
'ansible.plugins.action',
|
||||
C.DEFAULT_ACTION_PLUGIN_PATH,
|
||||
'action_plugins'
|
||||
'action_plugins',
|
||||
required_base_class='ActionBase',
|
||||
)
|
||||
|
||||
cache_loader = PluginLoader(
|
||||
'CacheModule',
|
||||
'ansible.cache',
|
||||
'ansible.plugins.cache',
|
||||
C.DEFAULT_CACHE_PLUGIN_PATH,
|
||||
'cache_plugins'
|
||||
'cache_plugins',
|
||||
)
|
||||
|
||||
callback_loader = PluginLoader(
|
||||
'CallbackModule',
|
||||
'ansible.callback_plugins',
|
||||
'ansible.plugins.callback',
|
||||
C.DEFAULT_CALLBACK_PLUGIN_PATH,
|
||||
'callback_plugins'
|
||||
'callback_plugins',
|
||||
)
|
||||
|
||||
connection_loader = PluginLoader(
|
||||
'Connection',
|
||||
'ansible.runner.connection_plugins',
|
||||
'ansible.plugins.connections',
|
||||
C.DEFAULT_CONNECTION_PLUGIN_PATH,
|
||||
'connection_plugins',
|
||||
aliases={'paramiko': 'paramiko_ssh'}
|
||||
aliases={'paramiko': 'paramiko_ssh'},
|
||||
required_base_class='ConnectionBase',
|
||||
)
|
||||
|
||||
shell_loader = PluginLoader(
|
||||
'ShellModule',
|
||||
'ansible.runner.shell_plugins',
|
||||
'ansible.plugins.shell',
|
||||
'shell_plugins',
|
||||
'shell_plugins',
|
||||
)
|
||||
|
||||
module_finder = PluginLoader(
|
||||
module_loader = PluginLoader(
|
||||
'',
|
||||
'ansible.modules',
|
||||
C.DEFAULT_MODULE_PATH,
|
||||
'library'
|
||||
'library',
|
||||
)
|
||||
|
||||
lookup_loader = PluginLoader(
|
||||
'LookupModule',
|
||||
'ansible.runner.lookup_plugins',
|
||||
'ansible.plugins.lookup',
|
||||
C.DEFAULT_LOOKUP_PLUGIN_PATH,
|
||||
'lookup_plugins'
|
||||
'lookup_plugins',
|
||||
required_base_class='LookupBase',
|
||||
)
|
||||
|
||||
vars_loader = PluginLoader(
|
||||
'VarsModule',
|
||||
'ansible.inventory.vars_plugins',
|
||||
'ansible.plugins.vars',
|
||||
C.DEFAULT_VARS_PLUGIN_PATH,
|
||||
'vars_plugins'
|
||||
'vars_plugins',
|
||||
)
|
||||
|
||||
filter_loader = PluginLoader(
|
||||
'FilterModule',
|
||||
'ansible.runner.filter_plugins',
|
||||
'ansible.plugins.filter',
|
||||
C.DEFAULT_FILTER_PLUGIN_PATH,
|
||||
'filter_plugins'
|
||||
'filter_plugins',
|
||||
)
|
||||
|
||||
fragment_loader = PluginLoader(
|
||||
@@ -302,3 +378,11 @@ fragment_loader = PluginLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'module_docs_fragments'),
|
||||
'',
|
||||
)
|
||||
|
||||
strategy_loader = PluginLoader(
|
||||
'StrategyModule',
|
||||
'ansible.plugins.strategies',
|
||||
None,
|
||||
'strategy_plugins',
|
||||
required_base_class='StrategyBase',
|
||||
)
|
||||
468
lib/ansible/plugins/action/__init__.py
Normal file
468
lib/ansible/plugins/action/__init__.py
Normal file
@@ -0,0 +1,468 @@
|
||||
# (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
|
||||
from ansible.utils.unicode import to_bytes
|
||||
|
||||
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, templar, shared_loader_obj):
|
||||
self._task = task
|
||||
self._connection = connection
|
||||
self._connection_info = connection_info
|
||||
self._loader = loader
|
||||
self._templar = templar
|
||||
self._shared_loader_obj = shared_loader_obj
|
||||
|
||||
# load the shell plugin for this action/connection
|
||||
if self._connection_info.shell:
|
||||
shell_type = self._connection_info.shell
|
||||
elif hasattr(connection, '_shell'):
|
||||
shell_type = getattr(connection, '_shell')
|
||||
else:
|
||||
shell_type = os.path.basename(C.DEFAULT_EXECUTABLE)
|
||||
|
||||
self._shell = shell_loader.get(shell_type)
|
||||
if not self._shell:
|
||||
raise AnsibleError("Invalid shell type specified (%s), or the plugin for that shell type is missing." % shell_type)
|
||||
|
||||
self._supports_check_mode = True
|
||||
|
||||
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 isinstance(data, dict):
|
||||
data = jsonify(data)
|
||||
|
||||
afd, afile = tempfile.mkstemp()
|
||||
afo = os.fdopen(afd, 'w')
|
||||
try:
|
||||
data = to_bytes(data, errors='strict')
|
||||
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(templar=self._templar, 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)
|
||||
|
||||
46
lib/ansible/plugins/action/debug.py
Normal file
46
lib/ansible/plugins/action/debug.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# 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
|
||||
|
||||
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']):
|
||||
results = self._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)
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#
|
||||
# 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
|
||||
@@ -22,95 +24,81 @@ import traceback
|
||||
import tempfile
|
||||
import base64
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible import utils
|
||||
from ansible import errors
|
||||
from ansible import module_common
|
||||
from ansible.runner.return_data import ReturnData
|
||||
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(object):
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def __init__(self, runner):
|
||||
self.runner = runner
|
||||
|
||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
||||
def run(self, tmp=None, task_vars=dict()):
|
||||
''' handler for fetch operations '''
|
||||
|
||||
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'))
|
||||
# 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'))
|
||||
|
||||
# load up options
|
||||
options = {}
|
||||
if complex_args:
|
||||
options.update(complex_args)
|
||||
options.update(utils.parse_kv(module_args))
|
||||
source = options.get('src', None)
|
||||
dest = options.get('dest', None)
|
||||
flat = options.get('flat', False)
|
||||
flat = utils.boolean(flat)
|
||||
fail_on_missing = options.get('fail_on_missing', False)
|
||||
fail_on_missing = utils.boolean(fail_on_missing)
|
||||
validate_checksum = options.get('validate_checksum', None)
|
||||
if validate_checksum is not None:
|
||||
validate_checksum = utils.boolean(validate_checksum)
|
||||
# Alias for validate_checksum (old way of specifying it)
|
||||
validate_md5 = options.get('validate_md5', None)
|
||||
if validate_md5 is not None:
|
||||
validate_md5 = utils.boolean(validate_md5)
|
||||
if validate_md5 is None and validate_checksum is None:
|
||||
# Default
|
||||
validate_checksum = True
|
||||
elif validate_checksum is None:
|
||||
validate_checksum = validate_md5
|
||||
elif validate_md5 is not None and validate_checksum is not None:
|
||||
results = dict(failed=True, msg="validate_checksum and validate_md5 cannot both be specified")
|
||||
return ReturnData(conn, result=results)
|
||||
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:
|
||||
results = dict(failed=True, msg="src and dest are required")
|
||||
return ReturnData(conn=conn, result=results)
|
||||
return dict(failed=True, msg="src and dest are required")
|
||||
|
||||
source = conn.shell.join_path(source)
|
||||
source = self.runner._remote_expand_user(conn, source, tmp)
|
||||
source = self._shell.join_path(source)
|
||||
source = self._remote_expand_user(source, tmp)
|
||||
|
||||
# calculate checksum for the remote file
|
||||
remote_checksum = self.runner._remote_checksum(conn, tmp, source, inject)
|
||||
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.runner.become:
|
||||
slurpres = self.runner._execute_module(conn, tmp, 'slurp', 'src=%s' % source, inject=inject)
|
||||
if slurpres.is_successful():
|
||||
if slurpres.result['encoding'] == 'base64':
|
||||
remote_data = base64.b64decode(slurpres.result['content'])
|
||||
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 = utils.checksum_s(remote_data)
|
||||
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.result.get('source')
|
||||
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 conn.shell.join_path('a', ''):
|
||||
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("/"):
|
||||
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 = utils.path_dwim(self.runner.basedir, dest)
|
||||
dest = self._loader.path_dwim(dest)
|
||||
else:
|
||||
# files are saved in dest dir, with a subdir for each host, then the filename
|
||||
dest = "%s/%s/%s" % (utils.path_dwim(self.runner.basedir, dest), inject['inventory_hostname'], source_local)
|
||||
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("//","/")
|
||||
|
||||
@@ -130,10 +118,10 @@ class ActionModule(object):
|
||||
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 ReturnData(conn=conn, result=result)
|
||||
return result
|
||||
|
||||
# calculate checksum for the local file
|
||||
local_checksum = utils.checksum(dest)
|
||||
local_checksum = checksum(dest)
|
||||
|
||||
if remote_checksum != local_checksum:
|
||||
# create the containing directories, if needed
|
||||
@@ -142,32 +130,29 @@ class ActionModule(object):
|
||||
|
||||
# fetch the file and check for changes
|
||||
if remote_data is None:
|
||||
conn.fetch_file(source, dest)
|
||||
self._connection.fetch_file(source, dest)
|
||||
else:
|
||||
f = open(dest, 'w')
|
||||
f.write(remote_data)
|
||||
f.close()
|
||||
new_checksum = utils.secure_hash(dest)
|
||||
new_checksum = secure_hash(dest)
|
||||
# For backwards compatibility. We'll return None on FIPS enabled
|
||||
# systems
|
||||
try:
|
||||
new_md5 = utils.md5(dest)
|
||||
new_md5 = md5(dest)
|
||||
except ValueError:
|
||||
new_md5 = None
|
||||
|
||||
if validate_checksum and new_checksum != remote_checksum:
|
||||
result = dict(failed=True, md5sum=new_md5, msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)
|
||||
return ReturnData(conn=conn, result=result)
|
||||
result = dict(changed=True, md5sum=new_md5, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)
|
||||
return ReturnData(conn=conn, result=result)
|
||||
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 = utils.md5(dest)
|
||||
local_md5 = md5(dest)
|
||||
except ValueError:
|
||||
local_md5 = None
|
||||
|
||||
result = dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)
|
||||
return ReturnData(conn=conn, result=result)
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user