diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54ad717c30b..8e5ba4b543e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/RELEASES.txt b/RELEASES.txt
index 5da393e4041..c75923d90cb 100644
--- a/RELEASES.txt
+++ b/RELEASES.txt
@@ -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
diff --git a/VERSION b/VERSION
index eb49d7c7fdc..aec258df73d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.7
+0.8
diff --git a/docs/man/man1/ansible-playbook.1 b/docs/man/man1/ansible-playbook.1
index a3c14a323c4..8dcd4174d4b 100644
--- a/docs/man/man1/ansible-playbook.1
+++ b/docs/man/man1/ansible-playbook.1
@@ -1,22 +1,13 @@
'\" t
.\" Title: ansible-playbook
.\" Author: [see the "AUTHOR" section]
-.\" Generator: DocBook XSL Stylesheets v1.76.1
-.\" Date: 08/15/2012
+.\" Generator: DocBook XSL Stylesheets v1.75.2
+.\" 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\&.
diff --git a/docs/man/man1/ansible-playbook.1.asciidoc.in b/docs/man/man1/ansible-playbook.1.asciidoc.in
index fad82d82af1..0b57180586b 100644
--- a/docs/man/man1/ansible-playbook.1.asciidoc.in
+++ b/docs/man/man1/ansible-playbook.1.asciidoc.in
@@ -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
diff --git a/docs/man/man1/ansible-pull.1 b/docs/man/man1/ansible-pull.1
index 5eea65bede7..233aee72c94 100644
--- a/docs/man/man1/ansible-pull.1
+++ b/docs/man/man1/ansible-pull.1
@@ -1,22 +1,13 @@
'\" t
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
-.\" Generator: DocBook XSL Stylesheets v1.76.1
-.\" Date: 08/14/2012
+.\" Generator: DocBook XSL Stylesheets v1.75.2
+.\" 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
diff --git a/docs/man/man1/ansible.1 b/docs/man/man1/ansible.1
index 88384194bbb..7dfb951913c 100644
--- a/docs/man/man1/ansible.1
+++ b/docs/man/man1/ansible.1
@@ -1,22 +1,13 @@
'\" t
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
-.\" Generator: DocBook XSL Stylesheets v1.76.1
-.\" Date: 08/15/2012
+.\" Generator: DocBook XSL Stylesheets v1.75.2
+.\" 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 [\-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\&.
diff --git a/docs/man/man1/ansible.1.asciidoc.in b/docs/man/man1/ansible.1.asciidoc.in
index ac30dbf7e46..6821db44a0e 100644
--- a/docs/man/man1/ansible.1.asciidoc.in
+++ b/docs/man/man1/ansible.1.asciidoc.in
@@ -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
---------
diff --git a/examples/ansible.cfg b/examples/ansible.cfg
index ac7fdce3287..f29fd55b58a 100644
--- a/examples/ansible.cfg
+++ b/examples/ansible.cfg
@@ -21,7 +21,7 @@ module_name = command
remote_tmp = $HOME/.ansible/tmp
-# the default pattern for ansible-playbooks ("hosts:")
+# the default pattern for ansible-playbooks ("hosts:")
pattern = *
@@ -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
diff --git a/lib/ansible/__init__.py b/lib/ansible/__init__.py
index 1b55e52e3af..73f13b44c47 100644
--- a/lib/ansible/__init__.py
+++ b/lib/ansible/__init__.py
@@ -14,5 +14,5 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see .
-__version__ = '0.7'
+__version__ = '0.8'
__author__ = 'Michael DeHaan'
diff --git a/lib/ansible/callback_plugins/__init__.py b/lib/ansible/callback_plugins/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/ansible/callbacks.py b/lib/ansible/callbacks.py
index 445ab385688..cf223fef366 100644
--- a/lib/ansible/callbacks.py
+++ b/lib/ansible/callbacks.py
@@ -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')
diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py
index f38b5039492..149b25b55bd 100644
--- a/lib/ansible/constants.py
+++ b/lib/ansible/constants.py
@@ -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
diff --git a/lib/ansible/inventory/__init__.py b/lib/ansible/inventory/__init__.py
index 6e83064f83f..e41e9acb124 100644
--- a/lib/ansible/inventory/__init__.py
+++ b/lib/ansible/inventory/__init__.py
@@ -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 = []
@@ -96,7 +97,9 @@ class Inventory(object):
applied subsets.
"""
- # process patterns
+ # 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
diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py
index c2376564415..e95dd0d64ca 100644
--- a/lib/ansible/module_common.py
+++ b/lib/ansible/module_common.py
@@ -47,15 +47,24 @@ 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,
+ def __init__(self, argument_spec, bypass_checks=False, no_log=False,
check_invalid_arguments=True, mutually_exclusive=None, required_together=None,
required_one_of=None):
@@ -142,7 +151,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 '''
@@ -196,11 +205,26 @@ class AnsibleModule(object):
def _log_invocation(self):
''' log that ansible ran the module '''
- 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)
+ # 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__))
+ 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 ===
diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py
index 78a725fe51c..d461c35ab50 100644
--- a/lib/ansible/playbook/__init__.py
+++ b/lib/ansible/playbook/__init__.py
@@ -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
@@ -166,8 +171,8 @@ class PlayBook(object):
# if the playbook is invoked with --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) - (matched_tags_all | unmatched_tags_all)
- unknown_tags.discard('all')
-
+ unknown_tags.discard('all')
+
if len(unknown_tags) > 0:
unmatched_tags_all.discard('all')
msg = 'tag(s) not found in playbook: %s. possible values: %s'
@@ -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():
diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py
index cd927cf5595..1b507df0ecf 100644
--- a/lib/ansible/playbook/play.py
+++ b/lib/ansible/playbook/play.py
@@ -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)
diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py
index 4a2d005efbc..8f983b87aa2 100644
--- a/lib/ansible/runner/__init__.py
+++ b/lib/ansible/runner/__init__.py
@@ -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
diff --git a/lib/ansible/runner/action_plugins/assemble.py b/lib/ansible/runner/action_plugins/assemble.py
new file mode 100644
index 00000000000..d529caf4dfe
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/assemble.py
@@ -0,0 +1,43 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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')
+
diff --git a/lib/ansible/runner/action_plugins/async.py b/lib/ansible/runner/action_plugins/async.py
new file mode 100644
index 00000000000..19a5a186fdc
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/async.py
@@ -0,0 +1,53 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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
+ )
+
diff --git a/lib/ansible/runner/action_plugins/copy.py b/lib/ansible/runner/action_plugins/copy.py
new file mode 100644
index 00000000000..2747ad959be
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/copy.py
@@ -0,0 +1,87 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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')
+
diff --git a/lib/ansible/runner/action_plugins/fetch.py b/lib/ansible/runner/action_plugins/fetch.py
new file mode 100644
index 00000000000..a487094bbc6
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/fetch.py
@@ -0,0 +1,89 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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)
+
diff --git a/lib/ansible/runner/action_plugins/normal.py b/lib/ansible/runner/action_plugins/normal.py
new file mode 100644
index 00000000000..b5130a3d55f
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/normal.py
@@ -0,0 +1,47 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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)
+
+
diff --git a/lib/ansible/runner/action_plugins/raw.py b/lib/ansible/runner/action_plugins/raw.py
new file mode 100644
index 00000000000..ba52809e174
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/raw.py
@@ -0,0 +1,39 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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)
+ ))
+
diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py
new file mode 100644
index 00000000000..61f2da14718
--- /dev/null
+++ b/lib/ansible/runner/action_plugins/template.py
@@ -0,0 +1,81 @@
+# (c) 2012, Michael DeHaan
+#
+# 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 .
+
+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')
+
+
diff --git a/lib/ansible/runner/connection_plugins/__init__.py b/lib/ansible/runner/connection_plugins/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py
index ca0f1b455db..107d7061f92 100644
--- a/lib/ansible/utils.py
+++ b/lib/ansible/utils.py
@@ -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,8 +487,11 @@ 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))
- modules[name] = imp.load_source(name, path)
+ if not name.startswith("_"):
+ modules[name] = imp.load_source(name, path)
return modules
diff --git a/library/assemble b/library/assemble
index 400438cb0e9..479badf676e 100755
--- a/library/assemble
+++ b/library/assemble
@@ -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
diff --git a/library/command b/library/command
index 8aa9899fc51..585ebabd7b3 100755
--- a/library/command
+++ b/library/command
@@ -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] != '/':
diff --git a/library/copy b/library/copy
index be7e685a8ab..b4da56c9797 100755
--- a/library/copy
+++ b/library/copy
@@ -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))
diff --git a/library/lineinfile b/library/lineinfile
index 40fca34987a..8405dc6637d 100755
--- a/library/lineinfile
+++ b/library/lineinfile
@@ -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
#<>
diff --git a/library/postgresql_db b/library/postgresql_db
index 32ee9205909..fdccd14ddbd 100755
--- a/library/postgresql_db
+++ b/library/postgresql_db
@@ -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 != '' )
diff --git a/library/postgresql_user b/library/postgresql_user
index d883cc57cc8..8f85988770d 100755
--- a/library/postgresql_user
+++ b/library/postgresql_user
@@ -71,7 +71,7 @@ def user_delete(cursor, user):
cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete")
cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
return False
-
+
cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
return True
@@ -89,7 +89,7 @@ def get_table_privileges(cursor, user, table):
WHERE grantee=%s AND table_name=%s AND table_schema=%s'''
cursor.execute(query, (user, table, schema))
return set([x[0] for x in cursor.fetchall()])
-
+
def grant_table_privilege(cursor, user, table, priv):
prev_priv = get_table_privileges(cursor, user, table)
@@ -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 []
@@ -204,9 +206,9 @@ def parse_privs(privs, db):
type_ = 'table'
name, privileges = token.split(':', 1)
priv_set = set(x.strip() for x in privileges.split(','))
-
+
o_privs[type_][name] = priv_set
-
+
return o_privs
# ===========================================
@@ -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,27 +238,29 @@ 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")
-
- # To use defaults values, keyword arguments must be absent, so
+
+ # To use defaults values, keyword arguments must be absent, so
# check which values are empty and don't include in the **kw
# dictionary
params_map = {
"login_host":"host",
"login_user":"user",
"login_password":"password",
- "db":"database"
+ "port":"port",
+ "db":"database"
}
- kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems()
+ kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems()
if k in params_map and v != "" )
try:
db_connection = psycopg2.connect(**kw)
cursor = db_connection.cursor()
except Exception, e:
module.fail_json(msg="unable to connect to database: %s" % e)
-
+
kw = dict(user=user)
changed = False
user_removed = False
diff --git a/library/setup b/library/setup
index 005919157b1..5499c2f8576 100755
--- a/library/setup
+++ b/library/setup
@@ -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:
@@ -198,7 +216,7 @@ class LinuxHardware(Hardware):
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
# DMI bits
DMI_DICT = dict(
- form_factor = '/sys/devices/virtual/dmi/id/chassis_type',
+ form_factor = '/sys/devices/virtual/dmi/id/chassis_type',
product_name = '/sys/devices/virtual/dmi/id/product_name',
product_serial = '/sys/devices/virtual/dmi/id/product_serial',
product_uuid = '/sys/devices/virtual/dmi/id/product_uuid',
@@ -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
diff --git a/library/subversion b/library/subversion
index c8782216f54..1b659c76697 100755
--- a/library/subversion
+++ b/library/subversion
@@ -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'])
diff --git a/library/supervisorctl b/library/supervisorctl
index 2f33c37ecd1..880388abd5a 100755
--- a/library/supervisorctl
+++ b/library/supervisorctl
@@ -19,10 +19,11 @@
# along with Ansible. If not, see .
#
-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:
diff --git a/library/virt b/library/virt
index c1181d7a5d8..5d69e36769a 100755
--- a/library/virt
+++ b/library/virt
@@ -19,6 +19,8 @@ VIRT_FAILED = 1
VIRT_SUCCESS = 0
VIRT_UNAVAILABLE=2
+import sys
+
try:
import libvirt
except ImportError:
diff --git a/library/yum b/library/yum
index e2e4b149c95..20b7770c4ab 100755
--- a/library/yum
+++ b/library/yum
@@ -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
diff --git a/packaging/debian/changelog b/packaging/debian/changelog
index 08ba4e9736a..04a7ba6c00f 100644
--- a/packaging/debian/changelog
+++ b/packaging/debian/changelog
@@ -1,12 +1,18 @@
+ansible (0.8) unstable; urgency=low
+
+ * 0.8 release pending
+
+ -- Michael DeHaan Thu, 06 Aug 2012 18:50:01 -0400
+
ansible (0.7) unstable; urgency=low
- * 0.6 not released yet
+ * 0.7 update
- -- Michael DeHaan Mon, 06 Aug 2012 19:50:01 -0400
+ -- Michael DeHaan Thu, 06 Aug 2012 18:50:00 -0400
ansible (0.6) unstable; urgency=low
- * 0.5 update
+ * 0.6 update
-- Michael DeHaan Mon, 06 Aug 2012 19:50:00 -0400
diff --git a/packaging/port/sysutils/ansible/Makefile b/packaging/port/sysutils/ansible/Makefile
index 14ee5e5fdc9..29877e8c57e 100644
--- a/packaging/port/sysutils/ansible/Makefile
+++ b/packaging/port/sysutils/ansible/Makefile
@@ -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/
diff --git a/packaging/rpm/ansible.spec b/packaging/rpm/ansible.spec
index c28aa066c6c..87b46fc9ed2 100644
--- a/packaging/rpm/ansible.spec
+++ b/packaging/rpm/ansible.spec
@@ -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 - 0.7-0
-- not released yet
+* Thu Aug 7 2012 Michael DeHaan - 0.8-0
+- pending
+
+* Thu Aug 6 2012 Michael DeHaan - 0.7-0
+- Release of 0.7
* Mon Aug 6 2012 Michael DeHaan - 0.6-0
- Release of 0.6
diff --git a/setup.py b/setup.py
index bd4bee2b420..8d768919fcf 100644
--- a/setup.py
+++ b/setup.py
@@ -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=[
diff --git a/test/TestInventory.py b/test/TestInventory.py
index ec71f54bc10..048126cc784 100644
--- a/test/TestInventory.py
+++ b/test/TestInventory.py
@@ -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
diff --git a/test/TestPlayBook.py b/test/TestPlayBook.py
index 87f27910b89..eff544151a0 100644
--- a/test/TestPlayBook.py
+++ b/test/TestPlayBook.py
@@ -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'))