Merge remote branch 'upstream/devel' into devel

pull/1044/head
Aleksej Romanov 12 years ago
commit 61e9b27df2

@ -1,7 +1,24 @@
Ansible Changes By Release
==========================
0.7 "Panama" -- release pending -- I can barely see the roadmap from the heat coming off of it.
0.8 "Cathedral" -- release pending
Misc:
* is_set is available for use inside of an only_if expression: is_set('ansible_eth0') # etc
* removes= exists on command just like creates=
* postgresql modules now take an optional port= parameter
* /proc/cmdline info is now available in Linux facts
* public host key detection for OS X
* to_yaml and from_yaml are available as Jinja2 filters
* server side action code (template, etc) are now fully pluggable
* lineinfile module now uses 'search' not exact 'match' in regexes, making it much more intuitive and not needing regex syntax most of the time
* $group and $group_names are now accessible in with_items
* playbooks can import playbooks in other directories and then be able to import tasks relative to them
* ansible config file can also go in '.ansible.cfg' in cwd in addition to ~/.ansible.cfg and /etc/ansible/ansible.cfg
* fix for inventory hosts at API level when hosts spec is a list and not a colon delimited string
0.7 "Panama" -- Sept 6 2012
Module changes:
@ -11,7 +28,7 @@ Module changes:
* misc yum module fixes
* better changed=True/False detection in user module on older Linux distros
* nicer errors from modules when arguments are not key=value
* backup option on copy (backup=yes)
* backup option on copy (backup=yes), as well as template, assemble, and lineinfile
* file module will not recurse on directory properties
* yum module now workable without having repoquery installed, but doesn't support comparisons or list= if so
* setup module now detects interfaces with aliases

@ -1,7 +1,8 @@
Ansible Releases at a Glance
============================
0.7 "Panama" ---------- X-XX-2012
0.8 "Cathedral" ------- pending
0.7 "Panama" ---------- 9-06-2012
0.6 "Cabo" ------------ 8-06-2012
0.5 "Amsterdam" ------- 7-04-2012
0.4 "Unchained" ------- 5-23-2012

@ -1 +1 @@
0.7
0.8

@ -1,22 +1,13 @@
'\" t
.\" Title: ansible-playbook
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 08/15/2012
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 09/06/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.7
.\" Source: Ansible 0.8
.\" Language: English
.\"
.TH "ANSIBLE\-PLAYBOOK" "1" "08/15/2012" "Ansible 0\&.7" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH "ANSIBLE\-PLAYBOOK" "1" "09/06/2012" "Ansible 0\&.8" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
@ -117,6 +108,11 @@ Connection type to use\&. Possible options are
\fIlocal\fR
is mostly useful for crontab or kickstarts\&.
.RE
.PP
\fB\-l\fR \fISUBSET\fR, \fB\-\-limit=\fR\fISUBSET\fR
.RS 4
Further limits the selected host/group patterns\&.
.RE
.SH "ENVIRONMENT"
.sp
The following environment variables may specified\&.

@ -93,6 +93,9 @@ user name to run as.
Connection type to use. Possible options are 'paramiko' (SSH), 'ssh',
and 'local'. 'local' is mostly useful for crontab or kickstarts.
*-l* 'SUBSET', *--limit=*'SUBSET'::
Further limits the selected host/group patterns.
ENVIRONMENT

@ -1,22 +1,13 @@
'\" t
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 08/14/2012
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 09/06/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.7
.\" Source: Ansible 0.8
.\" Language: English
.\"
.TH "ANSIBLE" "1" "08/14/2012" "Ansible 0\&.7" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH "ANSIBLE" "1" "09/06/2012" "Ansible 0\&.8" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
@ -34,7 +25,7 @@ ansible-pull \- set up a remote copy of ansible on each managed node
ansible \-d DEST \-U URL [ \-C CHECKOUT ]
.SH "DESCRIPTION"
.sp
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \*(Aqremote things\*(Aq over SSH\&.
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \'remote things\' over SSH\&.
.sp
Use ansible\-pull to set up a remote copy of ansible on each managed node, each set to run via cron and update playbook source via git\&. This inverts the default \fBpush\fR architecture of ansible into a \fBpull\fR architecture, which has near\-limitless scaling potential\&.
.sp

@ -1,22 +1,13 @@
'\" t
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 08/15/2012
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 09/06/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.7
.\" Source: Ansible 0.8
.\" Language: English
.\"
.TH "ANSIBLE" "1" "08/15/2012" "Ansible 0\&.7" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH "ANSIBLE" "1" "09/06/2012" "Ansible 0\&.8" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
@ -34,7 +25,7 @@ ansible \- run a command somewhere else
ansible <host\-pattern> [\-f forks] [\-m module_name] [\-a args]
.SH "DESCRIPTION"
.sp
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \*(Aqremote things\*(Aq over SSH\&.
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \'remote things\' over SSH\&.
.SH "ARGUMENTS"
.PP
\fBhost\-pattern\fR
@ -82,7 +73,7 @@ to load modules from\&. The default is
\fI/usr/share/ansible\fR\&.
.RE
.PP
\fB\-a\fR \*(Aq\fIARGUMENTS\fR\*(Aq, \fB\-\-args=\fR\*(Aq\fIARGUMENTS\fR\*(Aq
\fB\-a\fR \'\fIARGUMENTS\fR\', \fB\-\-args=\fR\'\fIARGUMENTS\fR\'
.RS 4
The
\fIARGUMENTS\fR
@ -165,6 +156,11 @@ Connection type to use\&. Possible options are
\fIlocal\fR
is mostly useful for crontab or kickstarts\&.
.RE
.PP
\fB\-l\fR \fISUBSET\fR, \fB\-\-limit=\fR\fISUBSET\fR
.RS 4
Further limits the selected host/group patterns\&.
.RE
.SH "INVENTORY"
.sp
Ansible stores the hosts it can potentially operate on in an inventory file\&. The syntax is one host per line\&. Groups headers are allowed and are included on their own line, enclosed in square brackets that start the line\&.

@ -116,6 +116,9 @@ Use this file to authenticate the connection.
Connection type to use. Possible options are 'paramiko' (SSH), 'ssh',
and 'local'. 'local' is mostly useful for crontab or kickstarts.
*-l* 'SUBSET', *--limit=*'SUBSET'::
Further limits the selected host/group patterns.
INVENTORY
---------

@ -62,7 +62,7 @@ remote_port=22
#remote_user=root
# if set, always use this private key file for authentication, same as if passing
# --private-key-file to ansible or ansible-playbook
# --private-key to ansible or ansible-playbook
#private_key_file=/path/to/file

@ -14,5 +14,5 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
__version__ = '0.7'
__version__ = '0.8'
__author__ = 'Michael DeHaan'

@ -25,7 +25,7 @@ from ansible.color import stringc
dirname = os.path.dirname(__file__)
callbacks = utils.import_plugins(os.path.join(dirname, 'callback_plugins'))
callbacks = [ c.CallbackModule() for c in callbacks.values() if c.__name__ != '__init__' ]
callbacks = [ c.CallbackModule() for c in callbacks.values() ]
cowsay = None
if os.path.exists("/usr/bin/cowsay"):
@ -76,11 +76,12 @@ class AggregateStats(object):
prev = (getattr(self, what)).get(host, 0)
getattr(self, what)[host] = prev+1
def compute(self, runner_results, setup=False, poll=False):
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 ('failed' in value and bool(value['failed'])) or ('rc' in value and value['rc'] != 0):
if not ignore_errors and (('failed' in value and bool(value['failed'])) or
('rc' in value and value['rc'] != 0)):
self._increment('failures', host)
elif 'skipped' in value and bool(value['skipped']):
self._increment('skipped', host)
@ -235,8 +236,8 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
)
super(CliRunnerCallbacks, self).on_unreachable(host, res)
def on_skipped(self, host):
super(CliRunnerCallbacks, self).on_skipped(host, res)
def on_skipped(self, host, item=None):
super(CliRunnerCallbacks, self).on_skipped(host, item)
def on_error(self, host, err):
print >>sys.stderr, "err: [%s] => %s\n" % (host, err)
@ -265,9 +266,9 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
def _on_any(self, host, result):
result2 = result.copy()
result2.pop('invocation', None)
print host_report_msg(host, self.options.module_name, result, self.options.one_line)
print host_report_msg(host, self.options.module_name, result2, self.options.one_line)
if self.options.tree:
utils.write_tree_file(self.options.tree, host, utils.jsonify(result,format=True))
utils.write_tree_file(self.options.tree, host, utils.jsonify(result2,format=True))
########################################################################
@ -291,14 +292,14 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
def on_failed(self, host, results, ignore_errors=False):
results = results.copy()
results.pop('invocation', None)
results2 = results.copy()
results2.pop('invocation', None)
item = results.get('item', None)
item = results2.get('item', None)
if item:
msg = "failed: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(results))
msg = "failed: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(results2))
else:
msg = "failed: [%s] => %s" % (host, utils.jsonify(results))
msg = "failed: [%s] => %s" % (host, utils.jsonify(results2))
print stringc(msg, 'red')
if ignore_errors:
print stringc("...ignoring", 'yellow')
@ -307,12 +308,12 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
def on_ok(self, host, host_result):
item = host_result.get('item', None)
host_result = host_result.copy()
host_result.pop('invocation', None)
host_result2 = host_result.copy()
host_result2.pop('invocation', None)
# show verbose output for non-setup module results if --verbose is used
msg = ''
if not self.verbose or host_result.get("verbose_override",None) is not None:
if not self.verbose or host_result2.get("verbose_override",None) is not None:
if item:
msg = "ok: [%s] => (item=%s)" % (host,item)
else:
@ -321,13 +322,13 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
else:
# verbose ...
if item:
msg = "ok: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(host_result))
msg = "ok: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(host_result2))
else:
if 'ansible_job_id' not in host_result or 'finished' in host_result:
msg = "ok: [%s] => %s" % (host, utils.jsonify(host_result))
if 'ansible_job_id' not in host_result or 'finished' in host_result2:
msg = "ok: [%s] => %s" % (host, utils.jsonify(host_result2))
if msg != '':
if not 'changed' in host_result or not host_result['changed']:
if not 'changed' in host_result2 or not host_result['changed']:
print stringc(msg, 'green')
else:
print stringc(msg, 'yellow')
@ -353,7 +354,7 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
else:
msg = "skipping: [%s]" % host
print stringc(msg, 'yellow')
super(PlaybookRunnerCallbacks, self).on_skipped(host, item=None)
super(PlaybookRunnerCallbacks, self).on_skipped(host, item)
def on_no_hosts(self):
print stringc("no hosts matched or remaining\n", 'red')

@ -35,13 +35,16 @@ def get_config(p, section, key, env_var, default):
def load_config_file():
p = ConfigParser.ConfigParser()
path1 = os.path.expanduser(
os.environ.get('ANSIBLE_CONFIG', "~/.ansible.cfg"))
path2 = "/etc/ansible/ansible.cfg"
path1 = os.getcwd() + "/ansible.cfg"
path2 = os.path.expanduser(os.environ.get('ANSIBLE_CONFIG', "~/.ansible.cfg"))
path3 = "/etc/ansible/ansible.cfg"
if os.path.exists(path1):
p.read(path1)
elif os.path.exists(path2):
p.read(path2)
elif os.path.exists(path3):
p.read(path3)
else:
return None
return p

@ -35,7 +35,7 @@ class Inventory(object):
"""
__slots__ = [ 'host_list', 'groups', '_restriction', '_also_restriction', '_subset', '_is_script',
'parser', '_vars_per_host', '_vars_per_group', '_hosts_cache' ]
'parser', '_vars_per_host', '_vars_per_group', '_hosts_cache', '_groups_list' ]
def __init__(self, host_list=C.DEFAULT_HOST_LIST):
@ -49,6 +49,7 @@ class Inventory(object):
self._vars_per_host = {}
self._vars_per_group = {}
self._hosts_cache = {}
self._groups_list = {}
# the inventory object holds a list of groups
self.groups = []
@ -97,6 +98,8 @@ class Inventory(object):
"""
# process patterns
if isinstance(pattern, list):
pattern = ';'.join(pattern)
patterns = pattern.replace(";",":").split(":")
positive_patterns = [ p for p in patterns if not p.startswith("!") ]
negative_patterns = [ p for p in patterns if p.startswith("!") ]
@ -213,6 +216,17 @@ class Inventory(object):
continue
return results
def groups_list(self):
if not self._groups_list:
groups = {}
for g in self.groups:
groups[g.name] = [h.name for h in g.get_hosts()]
ancestors = g.get_ancestors()
for a in ancestors:
groups[a.name] = [h.name for h in a.get_hosts()]
self._groups_list = groups
return self._groups_list
def get_groups(self):
return self.groups

@ -47,12 +47,21 @@ import subprocess
import sys
import syslog
import types
import time
import shutil
try:
from hashlib import md5 as _md5
except ImportError:
from md5 import md5 as _md5
try:
from systemd import journal
has_journal = True
except ImportError:
import syslog
has_journal = False
class AnsibleModule(object):
def __init__(self, argument_spec, bypass_checks=False, no_log=False,
@ -196,11 +205,26 @@ class AnsibleModule(object):
def _log_invocation(self):
''' log that ansible ran the module '''
# Sanitize possible password argument when logging.
log_args = dict()
passwd_keys = ['password', 'login_password']
for param in self.params:
if param in passwd_keys:
log_args[param] = 'NOT_LOGGING_PASSWORD'
else:
log_args[param] = self.params[param]
if (has_journal):
journal_args = ["MESSAGE=Ansible module invoked", "MODULE=%s" % os.path.basename(__file__)]
for arg in log_args:
journal_args.append(arg.upper() + "=" + str(log_args[arg]))
journal.sendv(*journal_args)
else:
msg = ''
syslog.openlog('ansible-%s' % os.path.basename(__file__))
# Sanitize possible password argument when logging
log_args = re.sub(r'password=.+ (.*)', r"password=NOT_LOGGING_PASSWORD \1", self.args)
log_args = re.sub(r'login_password=.+ (.*)', r"login_password=NOT_LOGGING_PASSWORD \1", log_args)
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % log_args)
for arg in log_args:
msg = msg + arg + '=' + str(log_args[arg]) + ' '
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % msg)
def get_bin_path(self, arg, required=False, opt_dirs=[]):
'''
@ -274,6 +298,18 @@ class AnsibleModule(object):
infile.close()
return digest.hexdigest()
def backup_local(self, fn):
'''make a date-marked backup of the specified file, return True or False on success or failure'''
# backups named basename-YYYY-MM-DD@HH:MM~
ext = time.strftime("%Y-%m-%d@%H:%M~", time.localtime(time.time()))
backupdest = '%s.%s' % (fn, ext)
try:
shutil.copy2(fn, backupdest)
except shutil.Error, e:
self.fail_json(msg='Could not make backup of %s to %s: %s' % (fn, backupdest, e))
return backupdest
# == END DYNAMICALLY INSERTED CODE ===

@ -112,7 +112,7 @@ class PlayBook(object):
self.global_vars.update(self.inventory.get_group_variables('all'))
self.basedir = os.path.dirname(playbook)
self.playbook = self._load_playbook_from_file(playbook)
(self.playbook, self.play_basedirs) = self._load_playbook_from_file(playbook)
self.module_path = self.module_path + os.pathsep + os.path.join(self.basedir, "library")
# *****************************************************
@ -124,6 +124,7 @@ class PlayBook(object):
playbook_data = utils.parse_yaml_from_file(path)
accumulated_plays = []
play_basedirs = []
if type(playbook_data) != list:
raise errors.AnsibleError("parse error: playbooks must be formatted as a YAML list")
@ -134,13 +135,17 @@ class PlayBook(object):
if 'include' in play:
if len(play.keys()) == 1:
included_path = utils.path_dwim(self.basedir, play['include'])
accumulated_plays.extend(self._load_playbook_from_file(included_path))
(plays, basedirs) = self._load_playbook_from_file(included_path)
accumulated_plays.extend(plays)
play_basedirs.extend(basedirs)
else:
raise errors.AnsibleError("parse error: top level includes cannot be used with other directives: %s" % play)
else:
accumulated_plays.append(play)
play_basedirs.append(os.path.dirname(path))
return accumulated_plays
return (accumulated_plays, play_basedirs)
# *****************************************************
@ -152,8 +157,8 @@ class PlayBook(object):
# loop through all patterns and run them
self.callbacks.on_start()
for play_ds in self.playbook:
play = Play(self,play_ds)
for (play_ds, play_basedir) in zip(self.playbook, self.play_basedirs):
play = Play(self, play_ds, play_basedir)
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
@ -215,7 +220,7 @@ class PlayBook(object):
timeout=self.timeout, remote_user=task.play.remote_user,
remote_port=task.play.remote_port, module_vars=task.module_vars,
private_key_file=self.private_key_file,
setup_cache=self.SETUP_CACHE, basedir=self.basedir,
setup_cache=self.SETUP_CACHE, basedir=task.play.basedir,
conditional=task.only_if, callbacks=self.runner_callbacks,
sudo=task.play.sudo, sudo_user=task.play.sudo_user,
transport=task.play.transport, sudo_pass=self.sudo_pass, is_playbook=True
@ -246,7 +251,7 @@ class PlayBook(object):
if results is None:
results = {}
self.stats.compute(results)
self.stats.compute(results, ignore_errors=task.ignore_errors)
# add facts to the global setup cache
for host, result in results['contacted'].iteritems():

@ -29,7 +29,8 @@ class Play(object):
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
'handlers', 'remote_user', 'remote_port',
'sudo', 'sudo_user', 'transport', 'playbook',
'tags', 'gather_facts', 'serial', '_ds', '_handlers', '_tasks'
'tags', 'gather_facts', 'serial', '_ds', '_handlers', '_tasks',
'basedir'
]
# to catch typos and so forth -- these are userland names
@ -42,7 +43,7 @@ class Play(object):
# *************************************************
def __init__(self, playbook, ds):
def __init__(self, playbook, ds, basedir):
''' constructor loads from a play datastructure '''
for x in ds.keys():
@ -57,15 +58,15 @@ class Play(object):
elif isinstance(hosts, list):
hosts = ';'.join(hosts)
hosts = utils.template(hosts, playbook.extra_vars)
self._ds = ds
self.playbook = playbook
self.basedir = basedir
self.hosts = hosts
self.name = ds.get('name', self.hosts)
self.vars = ds.get('vars', {})
self.vars_files = ds.get('vars_files', [])
self.vars_prompt = ds.get('vars_prompt', {})
self.vars = self._get_vars(self.playbook.basedir)
self.vars = self._get_vars()
self._tasks = ds.get('tasks', [])
self._handlers = ds.get('handlers', [])
self.remote_user = utils.template(ds.get('user', self.playbook.remote_user), playbook.extra_vars)
@ -107,7 +108,7 @@ class Play(object):
(k,v) = t.split("=", 1)
task_vars[k] = utils.template(v, task_vars)
include_file = utils.template(tokens[0], task_vars)
data = utils.parse_yaml_from_file(utils.path_dwim(self.playbook.basedir, include_file))
data = utils.parse_yaml_from_file(utils.path_dwim(self.basedir, include_file))
elif type(x) == dict:
data = [x]
else:
@ -135,7 +136,7 @@ class Play(object):
# *************************************************
def _get_vars(self, dirname):
def _get_vars(self):
''' load the vars section from a play, accounting for all sorts of variable features
including loading from yaml files, prompting, and conditional includes of the first
file found in a list. '''
@ -264,7 +265,7 @@ class Play(object):
filename3 = filename2
if host is not None:
filename3 = utils.template(filename2, self.playbook.SETUP_CACHE[host])
filename4 = utils.path_dwim(self.playbook.basedir, filename3)
filename4 = utils.path_dwim(self.basedir, filename3)
sequence.append(filename4)
if os.path.exists(filename4):
found = True
@ -297,7 +298,7 @@ class Play(object):
filename3 = filename2
if host is not None:
filename3 = utils.template(filename2, self.playbook.SETUP_CACHE[host])
filename4 = utils.path_dwim(self.playbook.basedir, filename3)
filename4 = utils.path_dwim(self.basedir, filename3)
if self._has_vars_in(filename4):
return
new_vars = utils.parse_yaml_from_file(filename4)

@ -43,6 +43,9 @@ try:
except ImportError:
HAS_ATFORK=False
dirname = os.path.dirname(__file__)
action_plugins = utils.import_plugins(os.path.join(dirname, 'action_plugins'))
################################################
def _executor_hook(job_queue, result_queue):
@ -136,6 +139,11 @@ class Runner(object):
# ensure we are using unique tmp paths
random.seed()
# instantiate plugin classes
self.action_plugins = {}
for (k,v) in action_plugins.iteritems():
self.action_plugins[k] = v.ActionModule(self)
# *****************************************************
def _delete_remote_files(self, conn, files):
@ -209,217 +217,6 @@ class Runner(object):
# *****************************************************
def _execute_raw(self, conn, tmp, inject=None):
''' execute a non-module command for bootstrapping, or if there's no python on a device '''
return ReturnData(conn=conn, result=dict(
stdout=self._low_level_exec_command(conn, self.module_args.encode('utf-8'), tmp, sudoable = True)
))
# ***************************************************
def _execute_normal_module(self, conn, tmp, module_name, inject=None):
''' transfer & execute a module that is not 'copy' or 'template' '''
# shell and command are the same module
if module_name == 'shell':
module_name = 'command'
self.module_args += " #USE_SHELL"
vv("REMOTE_MODULE %s %s" % (module_name, self.module_args), host=conn.host)
exec_rc = self._execute_module(conn, tmp, module_name, self.module_args, inject=inject)
return exec_rc
# *****************************************************
def _execute_async_module(self, conn, tmp, module_name, inject=None):
''' transfer the given module name, plus the async module, then run it '''
# shell and command module are the same
module_args = self.module_args
if module_name == 'shell':
module_name = 'command'
module_args += " #USE_SHELL"
(module_path, is_new_style) = self._copy_module(conn, tmp, module_name, inject)
self._low_level_exec_command(conn, "chmod a+rx %s" % module_path, tmp)
return self._execute_module(conn, tmp, 'async_wrapper', module_args,
async_module=module_path,
async_jid=self.generated_jid,
async_limit=self.background,
inject=inject
)
# *****************************************************
def _execute_copy(self, conn, tmp, inject=None):
''' handler for file transfer operations '''
# load up options
options = utils.parse_kv(self.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if (source is None and not 'first_available_file' in inject) or dest is None:
result=dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, result=result)
# if we have first_available_file in our vars
# look up the files and use the first one we find as src
if 'first_available_file' in inject:
found = False
for fn in inject.get('first_available_file'):
fn = utils.template(fn, inject)
if os.path.exists(fn):
source = fn
found = True
break
if not found:
results=dict(failed=True, msg="could not find src in first_available_file list")
return ReturnData(conn=conn, results=results)
source = utils.template(source, inject)
source = utils.path_dwim(self.basedir, source)
local_md5 = utils.md5(source)
if local_md5 is None:
result=dict(failed=True, msg="could not find src=%s" % source)
return ReturnData(conn=conn, result=result)
remote_md5 = self._remote_md5(conn, tmp, dest)
exec_rc = None
if local_md5 != remote_md5:
# transfer the file to a remote tmp location
tmp_src = tmp + os.path.basename(source)
conn.put_file(source, tmp_src)
# fix file permissions when the copy is done as a different user
if self.sudo and self.sudo_user != 'root':
self._low_level_exec_command(conn, "chmod a+r %s" % tmp_src, tmp)
# run the copy module
self.module_args = "%s src=%s" % (self.module_args, tmp_src)
return self._execute_module(conn, tmp, 'copy', self.module_args, inject=inject).daisychain('file')
else:
# no need to transfer the file, already correct md5
result = dict(changed=False, md5sum=remote_md5, transferred=False)
return ReturnData(conn=conn, result=result).daisychain('file')
# *****************************************************
def _execute_fetch(self, conn, tmp, inject=None):
''' handler for fetch operations '''
# load up options
options = utils.parse_kv(self.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if source is None or dest is None:
results = dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, result=results)
# apply templating to source argument
source = utils.template(source, inject)
# apply templating to dest argument
dest = utils.template(dest, inject)
# files are saved in dest dir, with a subdir for each host, then the filename
dest = "%s/%s/%s" % (utils.path_dwim(self.basedir, dest), conn.host, source)
dest = dest.replace("//","/")
# calculate md5 sum for the remote file
remote_md5 = self._remote_md5(conn, tmp, source)
# these don't fail because you may want to transfer a log file that possibly MAY exist
# but keep going to fetch other log files
if remote_md5 == '0':
result = dict(msg="unable to calculate the md5 sum of the remote file", file=source, changed=False)
return ReturnData(conn=conn, result=result)
if remote_md5 == '1':
result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False)
return ReturnData(conn=conn, result=result)
if remote_md5 == '2':
result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False)
return ReturnData(conn=conn, result=result)
# calculate md5 sum for the local file
local_md5 = utils.md5(dest)
if remote_md5 != local_md5:
# create the containing directories, if needed
if not os.path.isdir(os.path.dirname(dest)):
os.makedirs(os.path.dirname(dest))
# fetch the file and check for changes
conn.fetch_file(source, dest)
new_md5 = utils.md5(dest)
if new_md5 != remote_md5:
result = dict(failed=True, md5sum=new_md5, msg="md5 mismatch", file=source)
return ReturnData(conn=conn, result=result)
result = dict(changed=True, md5sum=new_md5)
return ReturnData(conn=conn, result=result)
else:
result = dict(changed=False, md5sum=local_md5, file=source)
return ReturnData(conn=conn, result=result)
# *****************************************************
def _execute_template(self, conn, tmp, inject=None):
''' handler for template operations '''
if not self.is_playbook:
raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks")
# load up options
options = utils.parse_kv(self.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if (source is None and 'first_available_file' not in inject) or dest is None:
result = dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, comm_ok=False, result=result)
# if we have first_available_file in our vars
# look up the files and use the first one we find as src
if 'first_available_file' in inject:
found = False
for fn in self.module_vars.get('first_available_file'):
fn = utils.template(fn, inject)
if os.path.exists(fn):
source = fn
found = True
break
if not found:
result = dict(failed=True, msg="could not find src in first_available_file list")
return ReturnData(conn=conn, comm_ok=False, result=result)
source = utils.template(source, inject)
# template the source data locally & transfer
try:
resultant = utils.template_from_file(self.basedir, source, inject)
except Exception, e:
result = dict(failed=True, msg=str(e))
return ReturnData(conn=conn, comm_ok=False, result=result)
xfered = self._transfer_str(conn, tmp, 'source', resultant)
# run the copy module, queue the file module
self.module_args = "%s src=%s dest=%s" % (self.module_args, xfered, dest)
return self._execute_module(conn, tmp, 'copy', self.module_args, inject=inject).daisychain('file')
# *****************************************************
def _execute_assemble(self, conn, tmp, inject=None):
''' handler for assemble operations '''
# FIXME: once assemble is ported over to the use the new common logic, this method
# will be unneccessary as it can decide to daisychain via it's own module returns.
# and this function can be deleted.
return self._execute_module(conn, tmp, 'assemble', self.module_args, inject=inject).daisychain('file')
# *****************************************************
def _executor(self, host):
''' handler for multiprocessing library '''
@ -453,17 +250,15 @@ class Runner(object):
inject.update(self.module_vars)
inject.update(self.setup_cache[host])
inject['hostvars'] = self.setup_cache
inject['group_names'] = host_variables.get('group_names', [])
inject['groups'] = self.inventory.groups_list()
# allow with_items to work in playbooks...
# apt and yum are converted into a single call, others run in a loop
items = self.module_vars.get('items', [])
if isinstance(items, basestring) and items.startswith("$"):
items = items.replace("$","")
if items in inject:
items = inject[items]
else:
raise errors.AnsibleError("unbound variable in with_items: %s" % items)
items = utils.varLookup(items, inject)
if type(items) != list:
raise errors.AnsibleError("with_items only takes a list: %s" % items)
@ -524,11 +319,7 @@ class Runner(object):
# 'hostvars' variable contains variables for each host name
# ... and is set elsewhere
# 'inventory_hostname' is also set elsewhere
group_hosts = {}
for g in self.inventory.groups:
group_hosts[g.name] = [ h.name for h in g.hosts ]
inject['groups'] = group_hosts
inject['groups'] = self.inventory.groups_list()
# allow module args to work as a dictionary
# though it is usually a string
new_args = ""
@ -538,8 +329,12 @@ class Runner(object):
self.module_args = new_args
self.module_args = utils.template(self.module_args, inject)
def _check_conditional(conditional):
def is_set(var):
return not var.startswith("$")
return eval(conditional)
conditional = utils.template(self.conditional, inject)
if not eval(conditional):
if not _check_conditional(conditional):
result = utils.jsonify(dict(skipped=True))
self.callbacks.on_skipped(host, inject.get('item',None))
return ReturnData(host=host, result=result)
@ -564,14 +359,14 @@ class Runner(object):
tmp = self._make_tmp_path(conn)
result = None
handler = getattr(self, "_execute_%s" % self.module_name, None)
handler = self.action_plugins.get(self.module_name, None)
if handler:
result = handler(conn, tmp, inject=inject)
result = handler.run(conn, tmp, module_name, inject)
else:
if self.background == 0:
result = self._execute_normal_module(conn, tmp, module_name, inject=inject)
result = self.action_plugins['normal'].run(conn, tmp, module_name, inject)
else:
result = self._execute_async_module(conn, tmp, module_name, inject=inject)
result = self.action_plugins['async'].run(conn, tmp, module_name, inject)
if result.is_successful() and 'daisychain' in result.result:
self.module_name = result.result['daisychain']
@ -616,11 +411,6 @@ class Runner(object):
elif not result.is_successful():
ignore_errors = self.module_vars.get('ignore_errors', False)
self.callbacks.on_failed(host, data, ignore_errors)
if ignore_errors:
if 'failed' in result.result:
result.result['failed'] = False
if 'rc' in result.result:
result.result['rc'] = 0
else:
self.callbacks.on_ok(host, data)
return result

@ -0,0 +1,43 @@
# (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 pwd
import random
import traceback
import tempfile
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
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject=None):
''' handler for assemble operations '''
# FIXME: since assemble is ported over to the use the new common logic, this method
# is actually unneccessary as it can decide to daisychain via it's own module returns.
# make assemble return daisychain_args and this will go away.
return self.runner._execute_module(conn, tmp, 'assemble', self.runner.module_args, inject=inject).daisychain('file')

@ -0,0 +1,53 @@
# (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 pwd
import random
import traceback
import tempfile
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
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject):
''' transfer the given module name, plus the async module, then run it '''
# shell and command module are the same
module_args = self.runner.module_args
if module_name == 'shell':
module_name = 'command'
module_args += " #USE_SHELL"
(module_path, is_new_style) = self.runner._copy_module(conn, tmp, module_name, inject)
self.runner._low_level_exec_command(conn, "chmod a+rx %s" % module_path, tmp)
return self.runner._execute_module(conn, tmp, 'async_wrapper', module_args,
async_module=module_path,
async_jid=self.runner.generated_jid,
async_limit=self.runner.background,
inject=inject
)

@ -0,0 +1,87 @@
# (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 pwd
import random
import traceback
import tempfile
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
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject):
''' handler for file transfer operations '''
# load up options
options = utils.parse_kv(self.runner.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if (source is None and not 'first_available_file' in inject) or dest is None:
result=dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, result=result)
# if we have first_available_file in our vars
# look up the files and use the first one we find as src
if 'first_available_file' in inject:
found = False
for fn in inject.get('first_available_file'):
fn = utils.template(fn, inject)
if os.path.exists(fn):
source = fn
found = True
break
if not found:
results=dict(failed=True, msg="could not find src in first_available_file list")
return ReturnData(conn=conn, results=results)
source = utils.template(source, inject)
source = utils.path_dwim(self.runner.basedir, source)
local_md5 = utils.md5(source)
if local_md5 is None:
result=dict(failed=True, msg="could not find src=%s" % source)
return ReturnData(conn=conn, result=result)
remote_md5 = self.runner._remote_md5(conn, tmp, dest)
exec_rc = None
if local_md5 != remote_md5:
# transfer the file to a remote tmp location
tmp_src = tmp + os.path.basename(source)
conn.put_file(source, tmp_src)
# fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % tmp_src, tmp)
# run the copy module
self.runner.module_args = "%s src=%s" % (self.runner.module_args, tmp_src)
return self.runner._execute_module(conn, tmp, 'copy', self.runner.module_args, inject=inject).daisychain('file')
else:
# no need to transfer the file, already correct md5
result = dict(changed=False, md5sum=remote_md5, transferred=False)
return ReturnData(conn=conn, result=result).daisychain('file')

@ -0,0 +1,89 @@
# (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 pwd
import random
import traceback
import tempfile
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
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject):
''' handler for fetch operations '''
# load up options
options = utils.parse_kv(self.runner.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if source is None or dest is None:
results = dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, result=results)
# apply templating to source argument
source = utils.template(source, inject)
# apply templating to dest argument
dest = utils.template(dest, inject)
# 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), conn.host, source)
dest = dest.replace("//","/")
# calculate md5 sum for the remote file
remote_md5 = self.runner._remote_md5(conn, tmp, source)
# these don't fail because you may want to transfer a log file that possibly MAY exist
# but keep going to fetch other log files
if remote_md5 == '0':
result = dict(msg="unable to calculate the md5 sum of the remote file", file=source, changed=False)
return ReturnData(conn=conn, result=result)
if remote_md5 == '1':
result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False)
return ReturnData(conn=conn, result=result)
if remote_md5 == '2':
result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False)
return ReturnData(conn=conn, result=result)
# calculate md5 sum for the local file
local_md5 = utils.md5(dest)
if remote_md5 != local_md5:
# create the containing directories, if needed
if not os.path.isdir(os.path.dirname(dest)):
os.makedirs(os.path.dirname(dest))
# fetch the file and check for changes
conn.fetch_file(source, dest)
new_md5 = utils.md5(dest)
if new_md5 != remote_md5:
result = dict(failed=True, md5sum=new_md5, msg="md5 mismatch", file=source)
return ReturnData(conn=conn, result=result)
result = dict(changed=True, md5sum=new_md5)
return ReturnData(conn=conn, result=result)
else:
result = dict(changed=False, md5sum=local_md5, file=source)
return ReturnData(conn=conn, result=result)

@ -0,0 +1,47 @@
# (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 pwd
import random
import traceback
import tempfile
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.callbacks import vv, vvv
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject):
''' transfer & execute a module that is not 'copy' or 'template' '''
# shell and command are the same module
if module_name == 'shell':
module_name = 'command'
self.runner.module_args += " #USE_SHELL"
vv("REMOTE_MODULE %s %s" % (module_name, self.runner.module_args), host=conn.host)
return self.runner._execute_module(conn, tmp, module_name, self.runner.module_args, inject=inject)

@ -0,0 +1,39 @@
# (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 pwd
import random
import traceback
import tempfile
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
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject):
return ReturnData(conn=conn, result=dict(
stdout=self.runner._low_level_exec_command(conn, self.runner.module_args.encode('utf-8'), tmp, sudoable=True)
))

@ -0,0 +1,81 @@
# (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 pwd
import random
import traceback
import tempfile
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
class ActionModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, inject):
''' handler for template operations '''
if not self.runner.is_playbook:
raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks")
# load up options
options = utils.parse_kv(self.runner.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if (source is None and 'first_available_file' not in inject) or dest is None:
result = dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, comm_ok=False, result=result)
# if we have first_available_file in our vars
# look up the files and use the first one we find as src
if 'first_available_file' in inject:
found = False
for fn in self.runner.module_vars.get('first_available_file'):
fn = utils.template(fn, inject)
if os.path.exists(fn):
source = fn
found = True
break
if not found:
result = dict(failed=True, msg="could not find src in first_available_file list")
return ReturnData(conn=conn, comm_ok=False, result=result)
source = utils.template(source, inject)
# template the source data locally & transfer
try:
resultant = utils.template_from_file(self.runner.basedir, source, inject)
except Exception, e:
result = dict(failed=True, msg=str(e))
return ReturnData(conn=conn, comm_ok=False, result=result)
xfered = self.runner._transfer_str(conn, tmp, 'source', resultant)
# fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered,
tmp)
# run the copy module, queue the file module
self.runner.module_args = "%s src=%s dest=%s" % (self.runner.module_args, xfered, dest)
return self.runner._execute_module(conn, tmp, 'copy', self.runner.module_args, inject=inject).daisychain('file')

@ -243,6 +243,8 @@ def template_from_file(basedir, path, vars):
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(basedir), trim_blocks=False)
environment.filters['to_json'] = json.dumps
environment.filters['from_json'] = json.loads
environment.filters['to_yaml'] = yaml.dump
environment.filters['from_yaml'] = yaml.load
data = codecs.open(path_dwim(basedir, path), encoding="utf8").read()
t = environment.from_string(data)
vars = vars.copy()
@ -323,7 +325,19 @@ def _gitinfo():
''' returns a string containing git branch, commit id and commit date '''
result = None
repo_path = os.path.join(os.path.dirname(__file__), '..', '..', '.git')
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.load(open(repo_path)).get('gitdir')
# There is a posibility the .git file to have an absolute path.
if os.path.isabs(gitdir):
repo_path = gitdir
else:
repo_path = os.path.join(repo_path.split('.git')[0], gitdir)
except (IOError, AttributeError):
return ''
f = open(os.path.join(repo_path, "HEAD"))
branch = f.readline().split('/')[-1].rstrip("\n")
f.close()
@ -340,7 +354,7 @@ def _gitinfo():
result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit,
time.strftime("%Y/%m/%d %H:%M:%S", date), offset / -36)
else:
result = 'n/a'
result = ''
return result
def version(prog):
@ -380,7 +394,7 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
default=constants.DEFAULT_HOST_LIST)
parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
help='ask for SSH password')
parser.add_option('--private-key', default=None, dest='private_key_file',
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('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
help='ask for sudo password')
@ -473,7 +487,10 @@ def filter_leading_non_json_lines(buf):
def import_plugins(directory):
modules = {}
for path in glob.glob(os.path.join(directory, '*.py')):
if path.startswith("_"):
continue
name, ext = os.path.splitext(os.path.basename(path))
if not name.startswith("_"):
modules[name] = imp.load_source(name, path)
return modules

@ -50,6 +50,7 @@ def main():
argument_spec = dict(
src = dict(required=True),
dest = dict(required=True),
backup=dict(default=False, choices=BOOLEANS),
)
)
@ -58,6 +59,7 @@ def main():
destmd5 = None
src = os.path.expanduser(module.params['src'])
dest = os.path.expanduser(module.params['dest'])
backup = module.boolean(module.params.get('backup', False))
if not os.path.exists(src):
module.fail_json(msg="Source (%s) does not exist" % src)
@ -72,6 +74,8 @@ def main():
destmd5 = module.md5(dest)
if pathmd5 != destmd5:
if backup and destmd5 is not None:
module.backup_local(dest)
shutil.copy(path, dest)
changed = True

@ -39,7 +39,7 @@ def main():
module.fail_json(msg="no command given")
if chdir:
os.chdir(chdir)
os.chdir(os.path.expanduser(chdir))
if not shell:
args = shlex.split(args)
@ -114,8 +114,24 @@ class CommandModule(AnsibleModule):
rc=0
)
args = args.replace(x,'')
elif x.startswith("removes="):
# do not run the command if the line contains removes=filename
# and the filename do not exists. This allows idempotence
# of command executions.
(k,v) = x.split("=",1)
if not os.path.exists(v):
self.exit_json(
cmd=args,
stdout="skipped, since %s do not exists" % v,
skipped=True,
changed=False,
stderr=False,
rc=0
)
args = args.replace(x,'')
elif x.startswith("chdir="):
(k,v) = x.split("=", 1)
v = os.path.expanduser(v)
if not (os.path.exists(v) and os.path.isdir(v)):
self.fail_json(msg="cannot change to directory '%s': path does not exist" % v)
elif v[0] != '/':

@ -20,19 +20,6 @@
import os
import shutil
import time
def backuplocal(fn):
"""make a date-marked backup of the specified file, return True or False on success or failure"""
# backups named basename-YYYY-MM-DD@HH:MM~
ext = time.strftime("%Y-%m-%d@%H:%M~", time.localtime(time.time()))
backupdest = '%s.%s' % (fn, ext)
try:
shutil.copy2(fn, backupdest)
except shutil.Error, e:
return False, 'Could not make backup of %s to %s: %s' % (fn, backupdest, e)
return True, backupdest
def main():
@ -76,11 +63,7 @@ def main():
try:
if backup:
if os.path.exists(dest):
success, msg = backuplocal(dest)
if not success:
module.fail_jason(msg=msg)
else:
backup_file = msg
backup_file = module.backup_local(dest)
shutil.copyfile(src, dest)
except shutil.Error:
module.fail_json(msg="failed to copy: %s and %s are the same" % (src, dest))

@ -21,14 +21,14 @@
import re
import os
def present(module, name, regexp, line, insertafter):
def present(module, name, regexp, line, insertafter, backup):
f = open(name, 'rb')
lines = f.readlines()
f.close()
mre = re.compile(regexp)
if not mre.match(line):
module.fail_json(msg="line= doesn't match regexp=")
if not mre.search(line):
module.fail_json(msg="usage error: line= doesn't match regexp (%s)" % regexp)
if insertafter in ('BOF', 'EOF'):
iare = None
@ -37,9 +37,9 @@ def present(module, name, regexp, line, insertafter):
index = [-1, -1]
for lineno in range(0, len(lines)):
if mre.match(lines[lineno]):
if mre.search(lines[lineno]):
index[0] = lineno
elif iare is not None and iare.match(lines[lineno]):
elif iare is not None and iare.search(lines[lineno]):
# + 1 for the next line
index[1] = lineno + 1
@ -69,20 +69,22 @@ def present(module, name, regexp, line, insertafter):
changed = True
if changed:
if backup:
module.backup_local(name)
f = open(name, 'wb')
f.writelines(lines)
f.close()
module.exit_json(changed=changed, msg=msg)
def absent(module, name, regexp):
def absent(module, name, regexp, backup):
f = open(name, 'rb')
lines = f.readlines()
f.close()
cre = re.compile(regexp)
found = []
def matcher(line):
if cre.match(line):
if cre.search(line):
found.append(line)
return False
else:
@ -90,6 +92,8 @@ def absent(module, name, regexp):
lines = filter(matcher, lines)
changed = len(found) > 0
if changed:
if backup:
module.backup_local(name)
f = open(name, 'wb')
f.writelines(lines)
f.close()
@ -103,18 +107,20 @@ def main():
regexp=dict(required=True),
line=dict(aliases=['value']),
insertafter=dict(default='EOF'),
backup=dict(default=False, choices=BOOLEANS),
),
)
params = module.params
backup = module.boolean(module.params.get('backup', False))
if params['state'] == 'present':
if 'line' not in params:
module.fail_json(msg='line= is required with state=present')
present(module, params['name'], params['regexp'], params['line'],
params['insertafter'])
params['insertafter'], backup)
else:
absent(module, params['name'], params['regexp'])
absent(module, params['name'], params['regexp'], backup)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

@ -77,6 +77,7 @@ def main():
login_user=dict(default="postgres"),
login_password=dict(default=""),
login_host=dict(default=""),
port=dict(default="5432"),
db=dict(required=True, aliases=['name']),
owner=dict(default=""),
template=dict(default=""),
@ -89,6 +90,7 @@ def main():
module.fail_json(msg="the python psycopg2 module is required")
db = module.params["db"]
port = module.params["port"]
owner = module.params["owner"]
template = module.params["template"]
encoding = module.params["encoding"]
@ -101,7 +103,8 @@ def main():
params_map = {
"login_host":"host",
"login_user":"user",
"login_password":"password"
"login_password":"password",
"port":"port"
}
kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems()
if k in params_map and v != '' )

@ -115,6 +115,8 @@ def get_database_privileges(cursor, user, db):
query = 'SELECT datacl FROM pg_database WHERE datname = %s'
cursor.execute(query, (db,))
datacl = cursor.fetchone()[0]
if datacl is None:
return []
r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl)
if r is None:
return []
@ -224,6 +226,7 @@ def main():
state=dict(default="present", choices=["absent", "present"]),
priv=dict(default=None),
db=dict(default=''),
port=dict(default='5432'),
fail_on_user=dict(default='yes')
)
)
@ -235,6 +238,7 @@ def main():
if db == '' and module.params["priv"] is not None:
module.fail_json(msg="privileges require a database to be specified")
privs = parse_privs(module.params["priv"], db)
port = module.params["port"]
if not postgresqldb_found:
module.fail_json(msg="the python psycopg2 module is required")
@ -246,6 +250,7 @@ def main():
"login_host":"host",
"login_user":"user",
"login_password":"password",
"port":"port",
"db":"database"
}
kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems()

@ -58,6 +58,7 @@ class Facts(object):
self.facts = {}
self.get_platform_facts()
self.get_distribution_facts()
self.get_cmdline()
self.get_public_ssh_host_keys()
self.get_selinux_facts()
@ -103,9 +104,26 @@ class Facts(object):
else:
self.facts['distribution'] = name
def get_cmdline(self):
data = get_file_content('/proc/cmdline')
if data:
self.facts['cmdline'] = {}
for piece in shlex.split(data):
item = piece.split('=', 1)
if len(item) == 1:
self.facts['cmdline'][item[0]] = True
else:
self.facts['cmdline'][item[0]] = item[1]
def get_public_ssh_host_keys(self):
dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
dsa_filename = '/etc/ssh/ssh_host_dsa_key.pub'
rsa_filename = '/etc/ssh/ssh_host_rsa_key.pub'
if self.facts['system'] == 'Darwin':
dsa_filename = '/etc/ssh_host_dsa_key.pub'
rsa_filename = '/etc/ssh_host_rsa_key.pub'
dsa = get_file_content(dsa_filename)
rsa = get_file_content(rsa_filename)
if dsa is None:
dsa = 'NA'
else:
@ -461,7 +479,9 @@ class LinuxNetwork(Network):
)
interface = dict(v4 = {}, v6 = {})
for v in 'v4', 'v6':
output = subprocess.Popen(command[v], stdout=subprocess.PIPE).communicate()[0]
if v == 'v6' and not socket.has_ipv6:
continue
output = subprocess.Popen(command[v], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0]
if not output:
# v6 routing may result in
# RTNETLINK answers: Invalid argument
@ -484,7 +504,7 @@ class LinuxNetwork(Network):
all_ipv4_addresses = [],
all_ipv6_addresses = [],
)
output = subprocess.Popen([ip_path, 'addr', 'show'], stdout=subprocess.PIPE).communicate()[0]
output = subprocess.Popen([ip_path, 'addr', 'show'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0]
for line in output.split('\n'):
if line:
words = line.split()
@ -517,9 +537,9 @@ class LinuxNetwork(Network):
# interface name for each address
if iface in interfaces:
i = 0
while '{0}_{1}'.format(iface, i) in interfaces:
while str(iface) + "_" + str(i) in interfaces:
i += 1
iface = '{0}_{1}'.format(iface, i)
iface = str(iface) + "_" + str(i)
interfaces[iface] = {}
interfaces[iface]['macaddress'] = macaddress

@ -99,7 +99,7 @@ def main():
)
)
dest = module.params['dest']
dest = os.path.expanduser(module.params['dest'])
repo = module.params['repo']
revision = module.params['revision']
force = module.boolean(module.params['force'])

@ -19,10 +19,11 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
def _is_present(name):
rc, out, err = _run('%s status' % _find_supervisorctl())
def _is_present(name, supervisorctl):
rc, out, err = _run('%s status' % supervisorctl)
return name in out
def _is_running(name, supervisorctl):
rc, out, err = _run('%s status %s' % (supervisorctl, name))
return 'RUNNING' in out
@ -49,10 +50,7 @@ def main():
SUPERVISORCTL = module.get_bin_path('supervisorctl', True)
if SUPERVISORCTL is None:
module.fail_json(msg='supervisorctl is not installed')
present = _is_present(name)
present = _is_present(name, SUPERVISORCTL)
if state == 'present':
if not present:

@ -19,6 +19,8 @@ VIRT_FAILED = 1
VIRT_SUCCESS = 0
VIRT_UNAVAILABLE=2
import sys
try:
import libvirt
except ImportError:

@ -291,7 +291,7 @@ def install(module, items, repoq, yum_basecmd, conf_file):
nvra = local_nvra(spec)
# look for them in the rpmdb
if is_installed(repoq, nvra, conf_file):
if is_installed(module, repoq, nvra, conf_file):
# if they are there, skip it
continue
pkg = spec

@ -1,12 +1,18 @@
ansible (0.8) unstable; urgency=low
* 0.8 release pending
-- Michael DeHaan <michael.dehaan@gmail.com> Thu, 06 Aug 2012 18:50:01 -0400
ansible (0.7) unstable; urgency=low
* 0.6 not released yet
* 0.7 update
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 06 Aug 2012 19:50:01 -0400
-- Michael DeHaan <michael.dehaan@gmail.com> Thu, 06 Aug 2012 18:50:00 -0400
ansible (0.6) unstable; urgency=low
* 0.5 update
* 0.6 update
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 06 Aug 2012 19:50:00 -0400

@ -1,7 +1,7 @@
# $FreeBSD$
PORTNAME= ansible
PORTVERSION= 0.6
PORTVERSION= 0.8
CATEGORIES= devel www textproc python
MASTER_SITES= https://github.com/downloads/ansible/ansible/

@ -6,7 +6,7 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Name: ansible
Release: 1%{?dist}
Summary: SSH-based configuration management, deployment, and task execution system
Version: 0.7
Version: 0.8
Group: Development/Libraries
License: GPLv3
@ -59,8 +59,11 @@ rm -rf $RPM_BUILD_ROOT
%doc examples/playbooks
%changelog
* Mon Aug 6 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.7-0
- not released yet
* Thu Aug 7 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.8-0
- pending
* Thu Aug 6 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.7-0
- Release of 0.7
* Mon Aug 6 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.6-0
- Release of 0.6

@ -28,7 +28,9 @@ setup(name='ansible',
'ansible.inventory',
'ansible.playbook',
'ansible.runner',
'ansible.runner.action_plugins',
'ansible.runner.connection_plugins',
'ansible.runner.action_plugins',
'ansible.callback_plugins',
],
scripts=[

@ -238,3 +238,11 @@ class TestInventory(unittest.TestCase):
'group_names': ['norse'],
'inventory_hostname': 'thor',
'inventory_hostname_short': 'thor'}
def test_hosts_list(self):
"""Test the case when playbook 'hosts' var is a list."""
inventory = self.script_inventory()
host_names = sorted(['thor', 'loki', 'odin']) # Not sure if sorting is in the contract or not
actual_hosts = inventory.get_hosts(host_names)
actual_host_names = [host.name for host in actual_hosts]
assert host_names == actual_host_names

@ -179,5 +179,5 @@ class TestPlaybook(unittest.TestCase):
callbacks=test_callbacks,
runner_callbacks=test_callbacks
)
play = ansible.playbook.Play(playbook, playbook.playbook[0])
play = ansible.playbook.Play(playbook, playbook.playbook[0], os.getcwd())
assert play.hosts == ';'.join(('host1', 'host2', 'host3'))

Loading…
Cancel
Save