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'))