Commit c5d2f6b0 by Michael DeHaan

implement lookup plugins for arbitrary enumeration over arbitrary things. See…

implement lookup plugins for arbitrary enumeration over arbitrary things.   See the mailing list for some cool examples.
parent 29d49d41
...@@ -8,6 +8,7 @@ Highlighted Core Changes: ...@@ -8,6 +8,7 @@ Highlighted Core Changes:
* fireball mode -- ansible can bootstrap a ephemeral 0mq (zeromq) daemon that runs as a given user and expires after X period of time. It is very fast. * fireball mode -- ansible can bootstrap a ephemeral 0mq (zeromq) daemon that runs as a given user and expires after X period of time. It is very fast.
* playbooks with errors now return 2 on failure. 1 indicates a more fatal syntax error. Similar for /usr/bin/ansible * playbooks with errors now return 2 on failure. 1 indicates a more fatal syntax error. Similar for /usr/bin/ansible
* server side action code (template, etc) are now fully pluggable * server side action code (template, etc) are now fully pluggable
* ability to write lookup plugins, like the code powering "with_fileglob" (see below)
Other Core Changes: Other Core Changes:
...@@ -44,6 +45,7 @@ Highlighted playbook changes: ...@@ -44,6 +45,7 @@ Highlighted playbook changes:
* when using a $list variable with $var or ${var} syntax it will automatically join with commas * when using a $list variable with $var or ${var} syntax it will automatically join with commas
* setup is not run more than once when we know it is has already been run in a play that included another play, etc * setup is not run more than once when we know it is has already been run in a play that included another play, etc
* can set/override sudo and sudo_user on individual tasks in a play, defaults to what is set in the play if not present * can set/override sudo and sudo_user on individual tasks in a play, defaults to what is set in the play if not present
* ability to use with_fileglob to iterate over local file patterns
Other playbook changes: Other playbook changes:
......
...@@ -26,6 +26,8 @@ from play import Play ...@@ -26,6 +26,8 @@ from play import Play
SETUP_CACHE = collections.defaultdict(dict) SETUP_CACHE = collections.defaultdict(dict)
plugins_dir = os.path.join(os.path.dirname(__file__), '..', 'runner')
class PlayBook(object): class PlayBook(object):
''' '''
runs an ansible playbook, given as a datastructure or YAML filename. runs an ansible playbook, given as a datastructure or YAML filename.
...@@ -105,9 +107,12 @@ class PlayBook(object): ...@@ -105,9 +107,12 @@ class PlayBook(object):
self.private_key_file = private_key_file self.private_key_file = private_key_file
self.only_tags = only_tags self.only_tags = only_tags
self.inventory = ansible.inventory.Inventory(host_list) self.inventory = ansible.inventory.Inventory(host_list)
self.inventory.subset(subset) self.inventory.subset(subset)
self.modules_list = utils.get_available_modules(self.module_path)
self.modules_list = utils.get_available_modules(self.module_path)
lookup_plugins_dir = os.path.join(plugins_dir, 'lookup_plugins')
self.lookup_plugins_list = utils.import_plugins(lookup_plugins_dir)
if not self.inventory._is_script: if not self.inventory._is_script:
self.global_vars.update(self.inventory.get_group_variables('all')) self.global_vars.update(self.inventory.get_group_variables('all'))
......
...@@ -26,7 +26,8 @@ class Task(object): ...@@ -26,7 +26,8 @@ class Task(object):
'notify', 'module_name', 'module_args', 'module_vars', 'notify', 'module_name', 'module_args', 'module_vars',
'play', 'notified_by', 'tags', 'register', 'with_items', 'play', 'notified_by', 'tags', 'register', 'with_items',
'delegate_to', 'first_available_file', 'ignore_errors', 'delegate_to', 'first_available_file', 'ignore_errors',
'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass' 'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass',
'items_lookup_plugin', 'items_lookup_terms'
] ]
# to prevent typos and such # to prevent typos and such
...@@ -40,11 +41,23 @@ class Task(object): ...@@ -40,11 +41,23 @@ class Task(object):
def __init__(self, play, ds, module_vars=None): def __init__(self, play, ds, module_vars=None):
''' constructor loads from a task or handler datastructure ''' ''' constructor loads from a task or handler datastructure '''
# code to allow for saying "modulename: args" versus "action: modulename args"
for x in ds.keys(): for x in ds.keys():
# code to allow for saying "modulename: args" versus "action: modulename args"
if x in play.playbook.modules_list: if x in play.playbook.modules_list:
ds['action'] = x + " " + ds.get(x, None) ds['action'] = x + " " + ds[x]
ds.pop(x) ds.pop(x)
# code to allow "with_glob" and to reference a lookup plugin named glob
elif x.startswith("with_") and x != 'with_items':
plugin_name = x.replace("with_","")
if plugin_name in play.playbook.lookup_plugins_list:
ds['items_lookup_plugin'] = plugin_name
ds['items_lookup_terms'] = ds[x]
ds.pop(x)
else:
raise errors.AnsibleError("cannot find lookup plugin named %s for usage in with_%s" % (plugin_name, plugin_name))
elif not x in Task.VALID_KEYS: elif not x in Task.VALID_KEYS:
raise errors.AnsibleError("%s is not a legal parameter in an Ansible task or handler" % x) raise errors.AnsibleError("%s is not a legal parameter in an Ansible task or handler" % x)
...@@ -101,6 +114,10 @@ class Task(object): ...@@ -101,6 +114,10 @@ class Task(object):
self.first_available_file = ds.get('first_available_file', None) self.first_available_file = ds.get('first_available_file', None)
self.with_items = ds.get('with_items', None) self.with_items = ds.get('with_items', None)
self.items_lookup_plugin = ds.get('items_lookup_plugin', None)
self.items_lookup_terms = ds.get('items_lookup_terms', None)
self.ignore_errors = ds.get('ignore_errors', False) self.ignore_errors = ds.get('ignore_errors', False)
# notify can be a string or a list, store as a list # notify can be a string or a list, store as a list
...@@ -125,8 +142,9 @@ class Task(object): ...@@ -125,8 +142,9 @@ class Task(object):
self.action = utils.template(None, self.action, self.module_vars) self.action = utils.template(None, self.action, self.module_vars)
# handle mutually incompatible options # handle mutually incompatible options
if self.with_items is not None and self.first_available_file is not None: incompatibles = [ x for x in [ self.with_items, self.first_available_file, self.items_lookup_plugin ] if x is not None ]
raise errors.AnsibleError("with_items and first_available_file are mutually incompatible in a single task") if len(incompatibles) > 1:
raise errors.AnsibleError("with_items, with_(plugin), and first_available_file are mutually incompatible in a single task")
# make first_available_file accessable to Runner code # make first_available_file accessable to Runner code
if self.first_available_file: if self.first_available_file:
...@@ -137,6 +155,10 @@ class Task(object): ...@@ -137,6 +155,10 @@ class Task(object):
self.with_items = [ ] self.with_items = [ ]
self.module_vars['items'] = self.with_items self.module_vars['items'] = self.with_items
if self.items_lookup_plugin is not None:
self.module_vars['items_lookup_plugin'] = self.items_lookup_plugin
self.module_vars['items_lookup_terms'] = self.items_lookup_terms
# allow runner to see delegate_to option # allow runner to see delegate_to option
self.module_vars['delegate_to'] = self.delegate_to self.module_vars['delegate_to'] = self.delegate_to
......
...@@ -47,7 +47,7 @@ except ImportError: ...@@ -47,7 +47,7 @@ except ImportError:
dirname = os.path.dirname(__file__) dirname = os.path.dirname(__file__)
action_plugin_list = utils.import_plugins(os.path.join(dirname, 'action_plugins')) action_plugin_list = utils.import_plugins(os.path.join(dirname, 'action_plugins'))
lookup_plugin_list = utils.import_plugins(os.path.join(dirname, 'lookup_plugins'))
################################################ ################################################
...@@ -71,8 +71,8 @@ def _executor_hook(job_queue, result_queue): ...@@ -71,8 +71,8 @@ def _executor_hook(job_queue, result_queue):
traceback.print_exc() traceback.print_exc()
class HostVars(dict): class HostVars(dict):
''' A special view of setup_cache that adds values from the inventory ''' A special view of setup_cache that adds values from the inventory when needed. '''
when needed. '''
def __init__(self, setup_cache, inventory): def __init__(self, setup_cache, inventory):
self.setup_cache = setup_cache self.setup_cache = setup_cache
self.inventory = inventory self.inventory = inventory
...@@ -82,9 +82,10 @@ class HostVars(dict): ...@@ -82,9 +82,10 @@ class HostVars(dict):
def __getitem__(self, host): def __getitem__(self, host):
if not host in self.lookup: if not host in self.lookup:
self.lookup[host] = self.inventory.get_variables(host) result = self.inventory.get_variables(host)
self.setup_cache[host].update(self.lookup[host]) result.update(self.setup_cache.get(host, {}))
return self.setup_cache[host] self.lookup[host] = result
return self.lookup[host]
class Runner(object): class Runner(object):
''' core API interface to ansible ''' ''' core API interface to ansible '''
...@@ -160,8 +161,11 @@ class Runner(object): ...@@ -160,8 +161,11 @@ class Runner(object):
# instantiate plugin classes # instantiate plugin classes
self.action_plugins = {} self.action_plugins = {}
self.lookup_plugins = {}
for (k,v) in action_plugin_list.iteritems(): for (k,v) in action_plugin_list.iteritems():
self.action_plugins[k] = v.ActionModule(self) self.action_plugins[k] = v.ActionModule(self)
for (k,v) in lookup_plugin_list.iteritems():
self.lookup_plugins[k] = v.LookupModule(self)
# ***************************************************** # *****************************************************
...@@ -189,7 +193,10 @@ class Runner(object): ...@@ -189,7 +193,10 @@ class Runner(object):
afd, afile = tempfile.mkstemp() afd, afile = tempfile.mkstemp()
afo = os.fdopen(afd, 'w') afo = os.fdopen(afd, 'w')
afo.write(data.encode('utf8')) try:
afo.write(data.encode('utf8'))
except:
raise errors.AnsibleError("failure encoding into utf-8")
afo.flush() afo.flush()
afo.close() afo.close()
...@@ -283,10 +290,18 @@ class Runner(object): ...@@ -283,10 +290,18 @@ class Runner(object):
# allow with_items to work in playbooks... # allow with_items to work in playbooks...
# apt and yum are converted into a single call, others run in a loop # apt and yum are converted into a single call, others run in a loop
items = self.module_vars.get('items', []) items = self.module_vars.get('items', [])
if isinstance(items, basestring) and items.startswith("$"): if isinstance(items, basestring) and items.startswith("$"):
items = utils.varReplaceWithItems(self.basedir, items, inject) items = utils.varReplaceWithItems(self.basedir, items, inject)
# if we instead said 'with_foo' and there is a lookup module named foo...
items_plugin = self.module_vars.get('items_lookup_plugin', None)
if items_plugin is not None:
items_terms = self.module_vars.get('items_lookup_terms', '')
if items_plugin in self.lookup_plugins:
items_terms = utils.template(self.basedir, items_terms, inject)
items = self.lookup_plugins[items_plugin].run(items_terms)
if type(items) != list: if type(items) != list:
raise errors.AnsibleError("with_items only takes a list: %s" % items) raise errors.AnsibleError("with_items only takes a list: %s" % items)
...@@ -313,6 +328,7 @@ class Runner(object): ...@@ -313,6 +328,7 @@ class Runner(object):
results.append(result.result) results.append(result.result)
if result.comm_ok == False: if result.comm_ok == False:
all_comm_ok = False all_comm_ok = False
all_failed = True
break break
for x in results: for x in results:
if x.get('changed') == True: if x.get('changed') == True:
...@@ -320,7 +336,7 @@ class Runner(object): ...@@ -320,7 +336,7 @@ class Runner(object):
if (x.get('failed') == True) or (('rc' in x) and (x['rc'] != 0)): if (x.get('failed') == True) or (('rc' in x) and (x['rc'] != 0)):
all_failed = True all_failed = True
break break
msg = 'All items succeeded' msg = 'All items completed'
if all_failed: if all_failed:
msg = "One or more items failed." msg = "One or more items failed."
rd_result = dict(failed=all_failed, changed=all_changed, results=results, msg=msg) rd_result = dict(failed=all_failed, changed=all_changed, results=results, msg=msg)
......
...@@ -39,6 +39,11 @@ class ActionModule(object): ...@@ -39,6 +39,11 @@ class ActionModule(object):
options = utils.parse_kv(module_args) options = utils.parse_kv(module_args)
source = options.get('src', None) source = options.get('src', None)
dest = options.get('dest', None) dest = options.get('dest', None)
if dest.endswith("/"):
base = os.path.basename(source)
dest = os.path.join(dest, base)
if (source is None and not 'first_available_file' in inject) or dest is 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") result=dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, result=result) return ReturnData(conn=conn, result=result)
...@@ -78,10 +83,16 @@ class ActionModule(object): ...@@ -78,10 +83,16 @@ class ActionModule(object):
# run the copy module # run the copy module
module_args = "%s src=%s" % (module_args, tmp_src) module_args = "%s src=%s" % (module_args, tmp_src)
print "CALLING FILE WITH: %s" % module_args
return self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject).daisychain('file', module_args) return self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject).daisychain('file', module_args)
else: else:
# no need to transfer the file, already correct md5 # no need to transfer the file, already correct md5, but still need to set src so the file module
# does not freak out. It's just the basename of the file.
tmp_src = tmp + os.path.basename(source)
module_args = "%s src=%s" % (module_args, tmp_src)
result = dict(changed=False, md5sum=remote_md5, transferred=False) result = dict(changed=False, md5sum=remote_md5, transferred=False)
print "CALLING FILE WITH: %s" % module_args
return ReturnData(conn=conn, result=result).daisychain('file', module_args) return ReturnData(conn=conn, result=result).daisychain('file', module_args)
...@@ -42,6 +42,11 @@ class ActionModule(object): ...@@ -42,6 +42,11 @@ class ActionModule(object):
options = utils.parse_kv(module_args) options = utils.parse_kv(module_args)
source = options.get('src', None) source = options.get('src', None)
dest = options.get('dest', None) dest = options.get('dest', None)
if dest.endswith("/"):
base = os.path.basename(source)
dest = os.path.join(dest, base)
if (source is None and 'first_available_file' not in inject) or dest is 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") result = dict(failed=True, msg="src and dest are required")
return ReturnData(conn=conn, comm_ok=False, result=result) return ReturnData(conn=conn, comm_ok=False, result=result)
......
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import os
import glob
class LookupModule(object):
def __init__(self, runner):
self.runner = runner
def run(self, terms):
return [ f for f in glob.glob(terms) if os.path.isfile(f) ]
...@@ -398,7 +398,10 @@ def template_from_file(basedir, path, vars): ...@@ -398,7 +398,10 @@ def template_from_file(basedir, path, vars):
environment.filters['from_json'] = json.loads environment.filters['from_json'] = json.loads
environment.filters['to_yaml'] = yaml.dump environment.filters['to_yaml'] = yaml.dump
environment.filters['from_yaml'] = yaml.load environment.filters['from_yaml'] = yaml.load
data = codecs.open(realpath, encoding="utf8").read() try:
data = codecs.open(realpath, encoding="utf8").read()
except:
raise errors.AnsibleError("unable to process as utf-8: %s" % realpath)
t = environment.from_string(data) t = environment.from_string(data)
vars = vars.copy() vars = vars.copy()
try: try:
...@@ -668,7 +671,8 @@ def filter_leading_non_json_lines(buf): ...@@ -668,7 +671,8 @@ def filter_leading_non_json_lines(buf):
def import_plugins(directory): def import_plugins(directory):
modules = {} modules = {}
for path in glob.glob(os.path.join(directory, '*.py')): python_files = os.path.join(directory, '*.py')
for path in glob.glob(python_files):
if path.startswith("_"): if path.startswith("_"):
continue continue
name, ext = os.path.splitext(os.path.basename(path)) name, ext = os.path.splitext(os.path.basename(path))
......
...@@ -89,9 +89,9 @@ def main(): ...@@ -89,9 +89,9 @@ def main():
module.fail_json(msg="Destination %s not writable" % (dest)) module.fail_json(msg="Destination %s not writable" % (dest))
if not os.access(dest, os.R_OK): if not os.access(dest, os.R_OK):
module.fail_json(msg="Destination %s not readable" % (dest)) module.fail_json(msg="Destination %s not readable" % (dest))
# Allow dest to be directory without compromising md5 check
if (os.path.isdir(dest)): if (os.path.isdir(dest)):
module.fail_json(msg="Destination %s cannot be a directory" % (dest)) basename = os.path.basename(src)
dest = os.path.join(dest, basename)
md5sum_dest = module.md5(dest) md5sum_dest = module.md5(dest)
else: else:
if not os.path.exists(os.path.dirname(dest)): if not os.path.exists(os.path.dirname(dest)):
......
...@@ -339,11 +339,18 @@ def main(): ...@@ -339,11 +339,18 @@ def main():
params = module.params params = module.params
state = params['state'] state = params['state']
path = os.path.expanduser(params['path']) path = os.path.expanduser(params['path'])
# source is both the source of a symlink or an informational passing of the src for a template module
# or copy module, even if this module never uses it, it is needed to key off some things
src = params.get('src', None) src = params.get('src', None)
if src: if src:
src = os.path.expanduser(src) src = os.path.expanduser(src)
force = module.boolean(params['force']) force = module.boolean(params['force'])
if src is not None and os.path.isdir(path):
params['path'] = path = os.path.join(path, os.path.basename(src))
mode = params.get('mode', None) mode = params.get('mode', None)
owner = params.get('owner', None) owner = params.get('owner', None)
group = params.get('group', None) group = params.get('group', None)
...@@ -370,6 +377,7 @@ def main(): ...@@ -370,6 +377,7 @@ def main():
changed = False changed = False
prev_state = 'absent' prev_state = 'absent'
if os.path.lexists(path): if os.path.lexists(path):
if os.path.islink(path): if os.path.islink(path):
prev_state = 'link' prev_state = 'link'
...@@ -392,7 +400,7 @@ def main(): ...@@ -392,7 +400,7 @@ def main():
module_exit_json(path=path, changed=True) module_exit_json(path=path, changed=True)
if prev_state != 'absent' and prev_state != state and not force: if prev_state != 'absent' and prev_state != state and not force:
module_fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state)) module_fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, src))
if prev_state == 'absent' and state == 'absent': if prev_state == 'absent' and state == 'absent':
module_exit_json(path=path, changed=False) module_exit_json(path=path, changed=False)
...@@ -400,7 +408,7 @@ def main(): ...@@ -400,7 +408,7 @@ def main():
if state == 'file': if state == 'file':
if prev_state != 'file': if prev_state != 'file':
module_fail_json(path=path, msg='file does not exist, use copy or template module to create') module_fail_json(path=path, msg='file (%s) does not exist, use copy or template module to create' % path)
# set modes owners and context as needed # set modes owners and context as needed
changed = set_context_if_different(path, secontext, changed) changed = set_context_if_different(path, secontext, changed)
......
...@@ -199,7 +199,6 @@ def serve(module, password, port, minutes): ...@@ -199,7 +199,6 @@ def serve(module, password, port, minutes):
# password as a variable in ansible is never logged though, so it serves well # password as a variable in ansible is never logged though, so it serves well
key = AesKey.Read(password) key = AesKey.Read(password)
log("DEBUG KEY=%s" % key) # REALLY NEED TO REMOVE THIS, DEBUG/DEV ONLY!
while True: while True:
......
...@@ -193,8 +193,11 @@ class TestRunner(unittest.TestCase): ...@@ -193,8 +193,11 @@ class TestRunner(unittest.TestCase):
assert self._run('file', ['dest=' + filedemo, 'state=file'])['failed'] assert self._run('file', ['dest=' + filedemo, 'state=file'])['failed']
assert os.path.isdir(filedemo) assert os.path.isdir(filedemo)
assert self._run('file', ['dest=' + filedemo, 'src=/dev/null', 'state=link'])['failed'] # this used to fail but will now make a 'null' symlink in the directory pointing to dev/null.
assert os.path.isdir(filedemo) # I feel this is ok but don't want to enforce it with a test.
#result = self._run('file', ['dest=' + filedemo, 'src=/dev/null', 'state=link'])
#assert result['failed']
#assert os.path.isdir(filedemo)
assert self._run('file', ['dest=' + filedemo, 'mode=701', 'state=directory'])['changed'] assert self._run('file', ['dest=' + filedemo, 'mode=701', 'state=directory'])['changed']
assert os.path.isdir(filedemo) and os.stat(filedemo).st_mode == 040701 assert os.path.isdir(filedemo) and os.stat(filedemo).st_mode == 040701
...@@ -245,7 +248,9 @@ class TestRunner(unittest.TestCase): ...@@ -245,7 +248,9 @@ class TestRunner(unittest.TestCase):
assert self._run('file', ['dest=' + filedemo, 'state=file', 'force=yes'])['failed'] assert self._run('file', ['dest=' + filedemo, 'state=file', 'force=yes'])['failed']
assert os.path.isdir(filedemo) assert os.path.isdir(filedemo)
assert self._run('file', ['dest=' + filedemo, 'src=/dev/null', 'state=link', 'force=yes'])['changed'] result = self._run('file', ['dest=' + filedemo, 'src=/dev/null', 'state=link', 'force=yes'])
assert result['changed']
print result
assert os.path.islink(filedemo) assert os.path.islink(filedemo)
os.unlink(filedemo) os.unlink(filedemo)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment