diff --git a/v2/ansible/errors/__init__.py b/v2/ansible/errors/__init__.py index e0c21d195bd..d4d93d0e4f0 100644 --- a/v2/ansible/errors/__init__.py +++ b/v2/ansible/errors/__init__.py @@ -48,10 +48,13 @@ class AnsibleError(Exception): if isinstance(self._obj, AnsibleBaseYAMLObject): extended_error = self._get_extended_error() if extended_error: - self.message = '%s\n%s' % (message, extended_error) + self.message = '%s\n\n%s' % (message, extended_error) else: self.message = message + def __str__(self): + return self.message + def __repr__(self): return self.message @@ -129,7 +132,7 @@ class AnsibleError(Exception): if unbalanced: error_message += YAML_COMMON_UNBALANCED_QUOTES_ERROR - except IOError: + except (IOError, TypeError): error_message += '\n(could not open file to display line)' except IndexError: error_message += '\n(specified line no longer in file, maybe it changed?)' diff --git a/v2/ansible/parsing/mod_args.py b/v2/ansible/parsing/mod_args.py index 7f4f42bddd2..0bb1c3fa2b3 100644 --- a/v2/ansible/parsing/mod_args.py +++ b/v2/ansible/parsing/mod_args.py @@ -63,8 +63,9 @@ class ModuleArgsParser: Args may also be munged for certain shell command parameters. """ - def __init__(self, task=None): - self._task = task + def __init__(self, task_ds=dict()): + assert isinstance(task_ds, dict) + self._task_ds = task_ds def _split_module_string(self, str): @@ -144,7 +145,7 @@ class ModuleArgsParser: # form is like: local_action: copy src=a dest=b ... pretty common args = parse_kv(thing) else: - raise AnsibleParsingError("unexpected parameter type in action: %s" % type(thing), obj=self._task) + raise AnsibleParsingError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds) return args def _normalize_new_style_args(self, thing): @@ -179,19 +180,17 @@ class ModuleArgsParser: else: # need a dict or a string, so giving up - raise AnsibleParsingError("unexpected parameter type in action: %s" % type(thing), obj=self._task) + raise AnsibleParsingError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds) return (action, args) - def parse(self, ds): + def parse(self): ''' Given a task in one of the supported forms, parses and returns returns the action, arguments, and delegate_to values for the task, dealing with all sorts of levels of fuzziness. ''' - assert isinstance(ds, dict) - thing = None action = None @@ -204,38 +203,38 @@ class ModuleArgsParser: # # action - if 'action' in ds: + if 'action' in self._task_ds: # an old school 'action' statement - thing = ds['action'] + thing = self._task_ds['action'] delegate_to = None action, args = self._normalize_parameters(thing) # local_action - if 'local_action' in ds: + if 'local_action' in self._task_ds: # local_action is similar but also implies a delegate_to if action is not None: - raise AnsibleParserError("action and local_action are mutually exclusive", obj=self._task) - thing = ds.get('local_action', '') + raise AnsibleParserError("action and local_action are mutually exclusive", obj=self._task_ds) + thing = self._task_ds.get('local_action', '') delegate_to = 'localhost' action, args = self._normalize_parameters(thing) # module: is the more new-style invocation # walk the input dictionary to see we recognize a module name - for (item, value) in iteritems(ds): + for (item, value) in iteritems(self._task_ds): if item in module_finder: # finding more than one module name is a problem if action is not None: - raise AnsibleParserError("conflicting action statements", obj=self._task) + raise AnsibleParserError("conflicting action statements", obj=self._task_ds) action = item thing = value action, args = self._normalize_parameters(value, action=action) # if we didn't see any module in the task at all, it's not a task really if action is None: - raise AnsibleParserError("no action detected in task", obj=self._task) + raise AnsibleParserError("no action detected in task", obj=self._task_ds) # shell modules require special handling (action, args) = self._handle_shell_weirdness(action, args) diff --git a/v2/ansible/parsing/yaml/__init__.py b/v2/ansible/parsing/yaml/__init__.py index 969fd2a3b55..4273abee539 100644 --- a/v2/ansible/parsing/yaml/__init__.py +++ b/v2/ansible/parsing/yaml/__init__.py @@ -27,6 +27,7 @@ from yaml import load, YAMLError from ansible.errors import AnsibleParserError from ansible.parsing.vault import VaultLib +from ansible.parsing.splitter import unquote from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject from ansible.parsing.yaml.strings import YAML_SYNTAX_ERROR @@ -55,6 +56,7 @@ class DataLoader(): _FILE_CACHE = dict() def __init__(self, vault_password=None): + self._basedir = '.' self._vault = VaultLib(password=vault_password) def load(self, data, file_name='', show_content=True): @@ -70,13 +72,15 @@ class DataLoader(): try: # if loading JSON failed for any reason, we go ahead # and try to parse it as YAML instead - return self._safe_load(data) + return self._safe_load(data, file_name=file_name) except YAMLError as yaml_exc: self._handle_error(yaml_exc, file_name, show_content) def load_from_file(self, file_name): ''' Loads data from a file, which can contain either JSON or YAML. ''' + file_name = self.path_dwim(file_name) + # if the file has already been read in and cached, we'll # return those results to avoid more file/vault operations if file_name in self._FILE_CACHE: @@ -100,9 +104,14 @@ class DataLoader(): def is_file(self, path): return os.path.isfile(path) - def _safe_load(self, stream): + def _safe_load(self, stream, file_name=None): ''' Implements yaml.safe_load(), except using our custom loader class. ''' - return load(stream, AnsibleLoader) + + loader = AnsibleLoader(stream, file_name) + try: + return loader.get_single_data() + finally: + loader.dispose() def _get_file_contents(self, file_name): ''' @@ -139,3 +148,23 @@ class DataLoader(): raise AnsibleParserError(YAML_SYNTAX_ERROR, obj=err_obj, show_content=show_content) + def set_basedir(self, basedir): + ''' sets the base directory, used to find files when a relative path is given ''' + + if basedir is not None: + self._basedir = basedir + + def path_dwim(self, given): + ''' + make relative paths work like folks expect. + ''' + + given = unquote(given) + + if given.startswith("/"): + return os.path.abspath(given) + elif given.startswith("~"): + return os.path.abspath(os.path.expanduser(given)) + else: + return os.path.abspath(os.path.join(self._basedir, given)) + diff --git a/v2/ansible/parsing/yaml/constructor.py b/v2/ansible/parsing/yaml/constructor.py index 1e94b808fa7..730ba85418f 100644 --- a/v2/ansible/parsing/yaml/constructor.py +++ b/v2/ansible/parsing/yaml/constructor.py @@ -23,6 +23,10 @@ from yaml.constructor import Constructor from ansible.parsing.yaml.objects import AnsibleMapping class AnsibleConstructor(Constructor): + def __init__(self, file_name=None): + self._ansible_file_name = file_name + super(AnsibleConstructor, self).__init__() + def construct_yaml_map(self, node): data = AnsibleMapping() yield data @@ -36,7 +40,16 @@ class AnsibleConstructor(Constructor): ret = AnsibleMapping(super(Constructor, self).construct_mapping(node, deep)) ret._line_number = node.__line__ ret._column_number = node.__column__ - ret._data_source = node.__datasource__ + + # in some cases, we may have pre-read the data and then + # passed it to the load() call for YAML, in which case we + # want to override the default datasource (which would be + # '') to the actual filename we read in + if self._ansible_file_name: + ret._data_source = self._ansible_file_name + else: + ret._data_source = node.__datasource__ + return ret AnsibleConstructor.add_constructor( diff --git a/v2/ansible/parsing/yaml/loader.py b/v2/ansible/parsing/yaml/loader.py index f75e5b4b276..0d130078190 100644 --- a/v2/ansible/parsing/yaml/loader.py +++ b/v2/ansible/parsing/yaml/loader.py @@ -28,11 +28,11 @@ from ansible.parsing.yaml.composer import AnsibleComposer from ansible.parsing.yaml.constructor import AnsibleConstructor class AnsibleLoader(Reader, Scanner, Parser, AnsibleComposer, AnsibleConstructor, Resolver): - def __init__(self, stream): + def __init__(self, stream, file_name=None): Reader.__init__(self, stream) Scanner.__init__(self) Parser.__init__(self) AnsibleComposer.__init__(self) - AnsibleConstructor.__init__(self) + AnsibleConstructor.__init__(self, file_name=file_name) Resolver.__init__(self) diff --git a/v2/ansible/parsing/yaml/objects.py b/v2/ansible/parsing/yaml/objects.py index 6a7482fe497..6eff9966f94 100644 --- a/v2/ansible/parsing/yaml/objects.py +++ b/v2/ansible/parsing/yaml/objects.py @@ -26,8 +26,8 @@ class AnsibleBaseYAMLObject: ''' _data_source = None - _line_number = None - _column_number = None + _line_number = 0 + _column_number = 0 def get_position_info(self): return (self._data_source, self._line_number, self._column_number) diff --git a/v2/ansible/parsing/yaml/strings.py b/v2/ansible/parsing/yaml/strings.py index b7e304194fc..dcd6ffd79fc 100644 --- a/v2/ansible/parsing/yaml/strings.py +++ b/v2/ansible/parsing/yaml/strings.py @@ -34,8 +34,8 @@ Syntax Error while loading YAML. """ YAML_POSITION_DETAILS = """\ -The error appears to have been in '%s': line %s, column %s, -but may actually be before there depending on the exact syntax problem. +The error appears to have been in '%s': line %s, column %s, but may +be elsewhere in the file depending on the exact syntax problem. """ YAML_COMMON_DICT_ERROR = """\ diff --git a/v2/ansible/playbook/__init__.py b/v2/ansible/playbook/__init__.py index 87b422b280a..f8f42b1163d 100644 --- a/v2/ansible/playbook/__init__.py +++ b/v2/ansible/playbook/__init__.py @@ -19,14 +19,60 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import os + +from ansible.errors import AnsibleError, AnsibleParserError +from ansible.parsing.yaml import DataLoader +from ansible.playbook.attribute import Attribute, FieldAttribute +from ansible.playbook.play import Play +from ansible.plugins import push_basedir + + +__all__ = ['Playbook'] + + class Playbook: - def __init__(self, filename): - self.ds = v2.utils.load_yaml_from_file(filename) - self.plays = [] - def load(self): - # loads a list of plays from the parsed ds - self.plays = [] + def __init__(self, loader=None): + # Entries in the datastructure of a playbook may + # be either a play or an include statement + self._entries = [] + self._basedir = '.' + + if loader: + self._loader = loader + else: + self._loader = DataLoader() + + @staticmethod + def load(file_name, loader=None): + pb = Playbook(loader=loader) + pb._load_playbook_data(file_name) + return pb + + def _load_playbook_data(self, file_name): + + # add the base directory of the file to the data loader, + # so that it knows where to find relatively pathed files + basedir = os.path.dirname(file_name) + self._loader.set_basedir(basedir) + + ds = self._loader.load_from_file(file_name) + if not isinstance(ds, list): + raise AnsibleParserError("playbooks must be a list of plays", obj=ds) + + # Parse the playbook entries. For plays, we simply parse them + # using the Play() object, and includes are parsed using the + # PlaybookInclude() object + for entry in ds: + if not isinstance(entry, dict): + raise AnsibleParserError("playbook entries must be either a valid play or an include statement", obj=entry) + + if 'include' in entry: + entry_obj = PlaybookInclude.load(entry, loader=self._loader) + else: + entry_obj = Play.load(entry, loader=self._loader) + + self._entries.append(entry_obj) + - def get_plays(self): - return self.plays diff --git a/v2/ansible/playbook/base.py b/v2/ansible/playbook/base.py index c1632403639..c7748095a5c 100644 --- a/v2/ansible/playbook/base.py +++ b/v2/ansible/playbook/base.py @@ -110,7 +110,7 @@ class Base: valid_attrs = [name for (name, attribute) in iteritems(self._get_base_attributes())] for key in ds: if key not in valid_attrs: - raise AnsibleParserError("'%s' is not a valid attribute for a %s" % (key, self.__class__), obj=ds) + raise AnsibleParserError("'%s' is not a valid attribute for a %s" % (key, self.__class__.__name__), obj=ds) def validate(self): ''' validation that is done at parse time, not load time ''' diff --git a/v2/ansible/playbook/block.py b/v2/ansible/playbook/block.py index cc5ccacc405..a082e97e5eb 100644 --- a/v2/ansible/playbook/block.py +++ b/v2/ansible/playbook/block.py @@ -67,15 +67,15 @@ class Block(Base): return ds def _load_block(self, attr, ds): - return load_list_of_tasks(ds) + return load_list_of_tasks(ds, block=self, loader=self._loader) def _load_rescue(self, attr, ds): - return load_list_of_tasks(ds) + return load_list_of_tasks(ds, block=self, loader=self._loader) def _load_always(self, attr, ds): - return load_list_of_tasks(ds) + return load_list_of_tasks(ds, block=self, loader=self._loader) # not currently used #def _load_otherwise(self, attr, ds): - # return self._load_list_of_tasks(ds) + # return self._load_list_of_tasks(ds, block=self, loader=self._loader) diff --git a/v2/ansible/playbook/helpers.py b/v2/ansible/playbook/helpers.py index 6985ad7808c..f692f4baf6c 100644 --- a/v2/ansible/playbook/helpers.py +++ b/v2/ansible/playbook/helpers.py @@ -17,6 +17,7 @@ from types import NoneType +from ansible.errors import AnsibleParserError def load_list_of_blocks(ds, role=None, loader=None): ''' @@ -38,24 +39,34 @@ def load_list_of_blocks(ds, role=None, loader=None): return block_list -def load_list_of_tasks(ds, block=None, role=None, loader=None): + +def load_list_of_tasks(ds, block=None, role=None, task_include=None, loader=None): ''' Given a list of task datastructures (parsed from YAML), - return a list of Task() objects. + return a list of Task() or TaskInclude() objects. ''' # we import here to prevent a circular dependency with imports from ansible.playbook.task import Task + from ansible.playbook.task_include import TaskInclude assert type(ds) == list task_list = [] for task in ds: - t = Task.load(task, block=block, role=role, loader=loader) + if not isinstance(task, dict): + raise AnsibleParserError("task/handler entries must be dictionaries (got a %s)" % type(task), obj=ds) + + if 'include' in task: + t = TaskInclude.load(task, block=block, role=role, task_include=task_include, loader=loader) + else: + t = Task.load(task, block=block, role=role, task_include=task_include, loader=loader) + task_list.append(t) return task_list + def load_list_of_roles(ds, loader=None): ''' Loads and returns a list of RoleInclude objects from the datastructure diff --git a/v2/ansible/playbook/play.py b/v2/ansible/playbook/play.py index 3c8a4bcb87f..07ee4707b40 100644 --- a/v2/ansible/playbook/play.py +++ b/v2/ansible/playbook/play.py @@ -124,28 +124,28 @@ class Play(Base): Loads a list of blocks from a list which may be mixed tasks/blocks. Bare tasks outside of a block are given an implicit block. ''' - return load_list_of_blocks(ds) + return load_list_of_blocks(ds, loader=self._loader) def _load_pre_tasks(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed tasks/blocks. Bare tasks outside of a block are given an implicit block. ''' - return load_list_of_blocks(ds) + return load_list_of_blocks(ds, loader=self._loader) def _load_post_tasks(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed tasks/blocks. Bare tasks outside of a block are given an implicit block. ''' - return load_list_of_blocks(ds) + return load_list_of_blocks(ds, loader=self._loader) def _load_handlers(self, attr, ds): ''' Loads a list of blocks from a list which may be mixed handlers/blocks. Bare handlers outside of a block are given an implicit block. ''' - return load_list_of_blocks(ds) + return load_list_of_blocks(ds, loader=self._loader) def _load_roles(self, attr, ds): ''' diff --git a/v2/ansible/playbook/role/__init__.py b/v2/ansible/playbook/role/__init__.py index 4950e944d3d..8f37970d59e 100644 --- a/v2/ansible/playbook/role/__init__.py +++ b/v2/ansible/playbook/role/__init__.py @@ -95,11 +95,11 @@ class Role: task_data = self._load_role_yaml('tasks') if task_data: - self._task_blocks = load_list_of_blocks(task_data) + self._task_blocks = load_list_of_blocks(task_data, role=self, loader=self._loader) handler_data = self._load_role_yaml('handlers') if handler_data: - self._handler_blocks = load_list_of_blocks(handler_data) + self._handler_blocks = load_list_of_blocks(handler_data, role=self, loader=self._loader) # vars and default vars are regular dictionaries self._role_vars = self._load_role_yaml('vars') diff --git a/v2/ansible/playbook/task.py b/v2/ansible/playbook/task.py index 97f7b06eb62..95571819af3 100644 --- a/v2/ansible/playbook/task.py +++ b/v2/ansible/playbook/task.py @@ -27,6 +27,7 @@ from ansible.errors import AnsibleError from ansible.parsing.splitter import parse_kv from ansible.parsing.mod_args import ModuleArgsParser from ansible.parsing.yaml import DataLoader +from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping from ansible.plugins import module_finder, lookup_finder class Task(Base): @@ -54,6 +55,7 @@ class Task(Base): _always_run = FieldAttribute(isa='bool') _any_errors_fatal = FieldAttribute(isa='bool') _async = FieldAttribute(isa='int') + _changed_when = FieldAttribute(isa='string') _connection = FieldAttribute(isa='string') _delay = FieldAttribute(isa='int') _delegate_to = FieldAttribute(isa='string') @@ -88,10 +90,13 @@ class Task(Base): _until = FieldAttribute(isa='list') # ? _when = FieldAttribute(isa='list', default=[]) - def __init__(self, block=None, role=None): + def __init__(self, block=None, role=None, task_include=None): ''' constructors a task, without the Task.load classmethod, it will be pretty blank ''' - self._block = block - self._role = role + + self._block = block + self._role = role + self._task_include = task_include + super(Task, self).__init__() def get_name(self): @@ -120,8 +125,8 @@ class Task(Base): return buf @staticmethod - def load(data, block=None, role=None, loader=None): - t = Task(block=block, role=role) + def load(data, block=None, role=None, task_include=None, loader=None): + t = Task(block=block, role=role, task_include=task_include) return t.load_data(data, loader=loader) def __repr__(self): @@ -131,9 +136,10 @@ class Task(Base): def _munge_loop(self, ds, new_ds, k, v): ''' take a lookup plugin name and store it correctly ''' - if self._loop.value is not None: - raise AnsibleError("duplicate loop in task: %s" % k) - new_ds['loop'] = k + loop_name = k.replace("with_", "") + if new_ds.get('loop') is not None: + raise AnsibleError("duplicate loop in task: %s" % loop_name) + new_ds['loop'] = loop_name new_ds['loop_args'] = v def munge(self, ds): @@ -147,13 +153,15 @@ class Task(Base): # the new, cleaned datastructure, which will have legacy # items reduced to a standard structure suitable for the # attributes of the task class - new_ds = dict() + new_ds = AnsibleMapping() + if isinstance(ds, AnsibleBaseYAMLObject): + new_ds.copy_position_info(ds) # use the args parsing class to determine the action, args, # and the delegate_to value from the various possible forms # supported as legacy - args_parser = ModuleArgsParser() - (action, args, delegate_to) = args_parser.parse(ds) + args_parser = ModuleArgsParser(task_ds=ds) + (action, args, delegate_to) = args_parser.parse() new_ds['action'] = action new_ds['args'] = args @@ -164,7 +172,7 @@ class Task(Base): # we don't want to re-assign these values, which were # determined by the ModuleArgsParser() above continue - elif "with_%s" % k in lookup_finder: + elif k.replace("with_", "") in lookup_finder: self._munge_loop(ds, new_ds, k, v) else: new_ds[k] = v diff --git a/v2/ansible/playbook/task_include.py b/v2/ansible/playbook/task_include.py index 785fc459921..798ce020d1c 100644 --- a/v2/ansible/playbook/task_include.py +++ b/v2/ansible/playbook/task_include.py @@ -19,3 +19,128 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +from ansible.errors import AnsibleParserError +from ansible.parsing.splitter import split_args, parse_kv +from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping +from ansible.playbook.attribute import Attribute, FieldAttribute +from ansible.playbook.base import Base +from ansible.playbook.helpers import load_list_of_tasks +from ansible.plugins import lookup_finder + + +__all__ = ['TaskInclude'] + + +class TaskInclude(Base): + + ''' + A class used to wrap the use of `include: /some/other/file.yml` + within a task list, which may return a list of Task objects and/or + more TaskInclude objects. + ''' + + # the description field is used mainly internally to + # show a nice reprsentation of this class, rather than + # simply using __class__.__name__ + + __desc__ = "task include statement" + + + #----------------------------------------------------------------- + # Attributes + + _include = FieldAttribute(isa='string') + _loop = FieldAttribute(isa='string', private=True) + _loop_args = FieldAttribute(isa='list', private=True) + _tags = FieldAttribute(isa='list', default=[]) + _vars = FieldAttribute(isa='dict', default=dict()) + _when = FieldAttribute(isa='list', default=[]) + + def __init__(self, block=None, role=None, task_include=None): + self._tasks = [] + self._block = block + self._role = role + self._task_include = task_include + + super(TaskInclude, self).__init__() + + @staticmethod + def load(data, block=None, role=None, task_include=None, loader=None): + ti = TaskInclude(block=block, role=role, task_include=None) + return ti.load_data(data, loader=loader) + + def munge(self, ds): + ''' + Regorganizes the data for a TaskInclude datastructure to line + up with what we expect the proper attributes to be + ''' + + assert isinstance(ds, dict) + + # the new, cleaned datastructure, which will have legacy + # items reduced to a standard structure + new_ds = AnsibleMapping() + if isinstance(ds, AnsibleBaseYAMLObject): + new_ds.copy_position_info(ds) + + for (k,v) in ds.iteritems(): + if k == 'include': + self._munge_include(ds, new_ds, k, v) + elif k.replace("with_", "") in lookup_finder: + self._munge_loop(ds, new_ds, k, v) + else: + # some basic error checking, to make sure vars are properly + # formatted and do not conflict with k=v parameters + # FIXME: we could merge these instead, but controlling the order + # in which they're encountered could be difficult + if k == 'vars': + if 'vars' in new_ds: + raise AnsibleParserError("include parameters cannot be mixed with 'vars' entries for include statements", obj=ds) + elif not isinstance(v, dict): + raise AnsibleParserError("vars for include statements must be specified as a dictionary", obj=ds) + new_ds[k] = v + + return new_ds + + def _munge_include(self, ds, new_ds, k, v): + ''' + Splits the include line up into filename and parameters + ''' + + # The include line must include at least one item, which is the filename + # to include. Anything after that should be regarded as a parameter to the include + items = split_args(v) + if len(items) == 0: + raise AnsibleParserError("include statements must specify the file name to include", obj=ds) + else: + # FIXME/TODO: validate that items[0] is a file, which also + # exists and is readable + new_ds['include'] = items[0] + if len(items) > 1: + # rejoin the parameter portion of the arguments and + # then use parse_kv() to get a dict of params back + params = parse_kv(" ".join(items[1:])) + if 'vars' in new_ds: + # FIXME: see fixme above regarding merging vars + raise AnsibleParserError("include parameters cannot be mixed with 'vars' entries for include statements", obj=ds) + new_ds['vars'] = params + + def _munge_loop(self, ds, new_ds, k, v): + ''' take a lookup plugin name and store it correctly ''' + + loop_name = k.replace("with_", "") + if new_ds.get('loop') is not None: + raise AnsibleError("duplicate loop in task: %s" % loop_name) + new_ds['loop'] = loop_name + new_ds['loop_args'] = v + + + def _load_include(self, attr, ds): + ''' loads the file name specified in the ds and returns a list of tasks ''' + + data = self._loader.load_from_file(ds) + if not isinstance(data, list): + raise AnsibleParsingError("included task files must contain a list of tasks", obj=ds) + + self._tasks = load_list_of_tasks(data, task_include=self, loader=self._loader) + return ds diff --git a/v2/ansible/plugins/lookup/csvfile.py b/v2/ansible/plugins/lookup/csvfile.py new file mode 100644 index 00000000000..ce5a2b77d2f --- /dev/null +++ b/v2/ansible/plugins/lookup/csvfile.py @@ -0,0 +1,82 @@ +# (c) 2013, Jan-Piet Mens +# +# 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 . + +from ansible import utils, errors +import os +import codecs +import csv + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def read_csv(self, filename, key, delimiter, dflt=None, col=1): + + try: + f = codecs.open(filename, 'r', encoding='utf-8') + creader = csv.reader(f, delimiter=delimiter) + + for row in creader: + if row[0] == key: + return row[int(col)] + except Exception, e: + raise errors.AnsibleError("csvfile: %s" % str(e)) + + return dflt + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if isinstance(terms, basestring): + terms = [ terms ] + + ret = [] + for term in terms: + params = term.split() + key = params[0] + + paramvals = { + 'file' : 'ansible.csv', + 'default' : None, + 'delimiter' : "TAB", + 'col' : "1", # column to return + } + + # parameters specified? + try: + for param in params[1:]: + name, value = param.split('=') + assert(name in paramvals) + paramvals[name] = value + except (ValueError, AssertionError), e: + raise errors.AnsibleError(e) + + if paramvals['delimiter'] == 'TAB': + paramvals['delimiter'] = "\t" + + path = utils.path_dwim(self.basedir, paramvals['file']) + + var = self.read_csv(path, key, paramvals['delimiter'], paramvals['default'], paramvals['col']) + if var is not None: + if type(var) is list: + for v in var: + ret.append(v) + else: + ret.append(var) + return ret diff --git a/v2/ansible/plugins/lookup/dict.py b/v2/ansible/plugins/lookup/dict.py new file mode 100644 index 00000000000..cda15465987 --- /dev/null +++ b/v2/ansible/plugins/lookup/dict.py @@ -0,0 +1,39 @@ +# (c) 2014, Kent R. Spillner +# +# 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 . + +from ansible.utils import safe_eval +import ansible.utils as utils +import ansible.errors as errors + +def flatten_hash_to_list(terms): + ret = [] + for key in terms: + ret.append({'key': key, 'value': terms[key]}) + return ret + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if not isinstance(terms, dict): + raise errors.AnsibleError("with_dict expects a dict") + + return flatten_hash_to_list(terms) diff --git a/v2/ansible/plugins/lookup/dnstxt.py b/v2/ansible/plugins/lookup/dnstxt.py new file mode 100644 index 00000000000..4fa47bf4ee9 --- /dev/null +++ b/v2/ansible/plugins/lookup/dnstxt.py @@ -0,0 +1,68 @@ +# (c) 2012, Jan-Piet Mens +# +# 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 . + +from ansible import utils, errors +import os +HAVE_DNS=False +try: + import dns.resolver + from dns.exception import DNSException + HAVE_DNS=True +except ImportError: + pass + +# ============================================================== +# DNSTXT: DNS TXT records +# +# key=domainname +# TODO: configurable resolver IPs +# -------------------------------------------------------------- + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + if HAVE_DNS == False: + raise errors.AnsibleError("Can't LOOKUP(dnstxt): module dns.resolver is not installed") + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if isinstance(terms, basestring): + terms = [ terms ] + + ret = [] + for term in terms: + domain = term.split()[0] + string = [] + try: + answers = dns.resolver.query(domain, 'TXT') + for rdata in answers: + s = rdata.to_text() + string.append(s[1:-1]) # Strip outside quotes on TXT rdata + + except dns.resolver.NXDOMAIN: + string = 'NXDOMAIN' + except dns.resolver.Timeout: + string = '' + except dns.exception.DNSException, e: + raise errors.AnsibleError("dns.resolver unhandled exception", e) + + ret.append(''.join(string)) + return ret diff --git a/v2/ansible/plugins/lookup/env.py b/v2/ansible/plugins/lookup/env.py new file mode 100644 index 00000000000..d4f85356edf --- /dev/null +++ b/v2/ansible/plugins/lookup/env.py @@ -0,0 +1,41 @@ +# (c) 2012, Jan-Piet Mens +# +# 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 . + +from ansible import utils, errors +from ansible.utils import template +import os + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + try: + terms = template.template(self.basedir, terms, inject) + except Exception, e: + pass + + if isinstance(terms, basestring): + terms = [ terms ] + + ret = [] + for term in terms: + var = term.split()[0] + ret.append(os.getenv(var, '')) + return ret diff --git a/v2/ansible/plugins/lookup/etcd.py b/v2/ansible/plugins/lookup/etcd.py new file mode 100644 index 00000000000..a758a2fb0b5 --- /dev/null +++ b/v2/ansible/plugins/lookup/etcd.py @@ -0,0 +1,78 @@ +# (c) 2013, Jan-Piet Mens +# +# 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 . + +from ansible import utils +import os +import urllib2 +try: + import json +except ImportError: + import simplejson as json + +# this can be made configurable, not should not use ansible.cfg +ANSIBLE_ETCD_URL = 'http://127.0.0.1:4001' +if os.getenv('ANSIBLE_ETCD_URL') is not None: + ANSIBLE_ETCD_URL = os.environ['ANSIBLE_ETCD_URL'] + +class etcd(): + def __init__(self, url=ANSIBLE_ETCD_URL): + self.url = url + self.baseurl = '%s/v1/keys' % (self.url) + + def get(self, key): + url = "%s/%s" % (self.baseurl, key) + + data = None + value = "" + try: + r = urllib2.urlopen(url) + data = r.read() + except: + return value + + try: + # {"action":"get","key":"/name","value":"Jane Jolie","index":5} + item = json.loads(data) + if 'value' in item: + value = item['value'] + if 'errorCode' in item: + value = "ENOENT" + except: + raise + pass + + return value + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + self.etcd = etcd() + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if isinstance(terms, basestring): + terms = [ terms ] + + ret = [] + for term in terms: + key = term.split()[0] + value = self.etcd.get(key) + ret.append(value) + return ret diff --git a/v2/ansible/plugins/lookup/file.py b/v2/ansible/plugins/lookup/file.py new file mode 100644 index 00000000000..70bae6653af --- /dev/null +++ b/v2/ansible/plugins/lookup/file.py @@ -0,0 +1,59 @@ +# (c) 2012, Daniel Hokka Zakrisson +# +# 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 . + +from ansible import utils, errors +import os +import codecs + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + ret = [] + + # this can happen if the variable contains a string, strictly not desired for lookup + # plugins, but users may try it, so make it work. + if not isinstance(terms, list): + terms = [ terms ] + + for term in terms: + basedir_path = utils.path_dwim(self.basedir, term) + relative_path = None + playbook_path = None + + # Special handling of the file lookup, used primarily when the + # lookup is done from a role. If the file isn't found in the + # basedir of the current file, use dwim_relative to look in the + # role/files/ directory, and finally the playbook directory + # itself (which will be relative to the current working dir) + if '_original_file' in inject: + relative_path = utils.path_dwim_relative(inject['_original_file'], 'files', term, self.basedir, check=False) + if 'playbook_dir' in inject: + playbook_path = os.path.join(inject['playbook_dir'], term) + + for path in (basedir_path, relative_path, playbook_path): + if path and os.path.exists(path): + ret.append(codecs.open(path, encoding="utf8").read().rstrip()) + break + else: + raise errors.AnsibleError("could not locate file in lookup: %s" % term) + + return ret diff --git a/v2/ansible/plugins/lookup/fileglob.py b/v2/ansible/plugins/lookup/fileglob.py new file mode 100644 index 00000000000..7d3cbb92be6 --- /dev/null +++ b/v2/ansible/plugins/lookup/fileglob.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 glob +from ansible import utils + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + ret = [] + + for term in terms: + + dwimmed = utils.path_dwim(self.basedir, term) + globbed = glob.glob(dwimmed) + ret.extend(g for g in globbed if os.path.isfile(g)) + + return ret diff --git a/v2/ansible/plugins/lookup/first_found.py b/v2/ansible/plugins/lookup/first_found.py new file mode 100644 index 00000000000..a48b56a3c28 --- /dev/null +++ b/v2/ansible/plugins/lookup/first_found.py @@ -0,0 +1,194 @@ +# (c) 2013, seth vidal red hat, inc +# +# 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 . + + +# take a list of files and (optionally) a list of paths +# return the first existing file found in the paths +# [file1, file2, file3], [path1, path2, path3] +# search order is: +# path1/file1 +# path1/file2 +# path1/file3 +# path2/file1 +# path2/file2 +# path2/file3 +# path3/file1 +# path3/file2 +# path3/file3 + +# first file found with os.path.exists() is returned +# no file matches raises ansibleerror +# EXAMPLES +# - name: copy first existing file found to /some/file +# action: copy src=$item dest=/some/file +# with_first_found: +# - files: foo ${inventory_hostname} bar +# paths: /tmp/production /tmp/staging + +# that will look for files in this order: +# /tmp/production/foo +# ${inventory_hostname} +# bar +# /tmp/staging/foo +# ${inventory_hostname} +# bar + +# - name: copy first existing file found to /some/file +# action: copy src=$item dest=/some/file +# with_first_found: +# - files: /some/place/foo ${inventory_hostname} /some/place/else + +# that will look for files in this order: +# /some/place/foo +# $relative_path/${inventory_hostname} +# /some/place/else + +# example - including tasks: +# tasks: +# - include: $item +# with_first_found: +# - files: generic +# paths: tasks/staging tasks/production +# this will include the tasks in the file generic where it is found first (staging or production) + +# example simple file lists +#tasks: +#- name: first found file +# action: copy src=$item dest=/etc/file.cfg +# with_first_found: +# - files: foo.${inventory_hostname} foo + + +# example skipping if no matched files +# First_found also offers the ability to control whether or not failing +# to find a file returns an error or not +# +#- name: first found file - or skip +# action: copy src=$item dest=/etc/file.cfg +# with_first_found: +# - files: foo.${inventory_hostname} +# skip: true + +# example a role with default configuration and configuration per host +# you can set multiple terms with their own files and paths to look through. +# consider a role that sets some configuration per host falling back on a default config. +# +#- name: some configuration template +# template: src={{ item }} dest=/etc/file.cfg mode=0444 owner=root group=root +# with_first_found: +# - files: +# - ${inventory_hostname}/etc/file.cfg +# paths: +# - ../../../templates.overwrites +# - ../../../templates +# - files: +# - etc/file.cfg +# paths: +# - templates + +# the above will return an empty list if the files cannot be found at all +# if skip is unspecificed or if it is set to false then it will return a list +# error which can be caught bye ignore_errors: true for that action. + +# finally - if you want you can use it, in place to replace first_available_file: +# you simply cannot use the - files, path or skip options. simply replace +# first_available_file with with_first_found and leave the file listing in place +# +# +# - name: with_first_found like first_available_file +# action: copy src=$item dest=/tmp/faftest +# with_first_found: +# - ../files/foo +# - ../files/bar +# - ../files/baz +# ignore_errors: true + + +from ansible import utils, errors +import os + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + result = None + anydict = False + skip = False + + for term in terms: + if isinstance(term, dict): + anydict = True + + total_search = [] + if anydict: + for term in terms: + if isinstance(term, dict): + files = term.get('files', []) + paths = term.get('paths', []) + skip = utils.boolean(term.get('skip', False)) + + filelist = files + if isinstance(files, basestring): + files = files.replace(',', ' ') + files = files.replace(';', ' ') + filelist = files.split(' ') + + pathlist = paths + if paths: + if isinstance(paths, basestring): + paths = paths.replace(',', ' ') + paths = paths.replace(':', ' ') + paths = paths.replace(';', ' ') + pathlist = paths.split(' ') + + if not pathlist: + total_search = filelist + else: + for path in pathlist: + for fn in filelist: + f = os.path.join(path, fn) + total_search.append(f) + else: + total_search.append(term) + else: + total_search = terms + + for fn in total_search: + if inject and '_original_file' in inject: + # check the templates and vars directories too, + # if they exist + for roledir in ('templates', 'vars'): + path = utils.path_dwim(os.path.join(self.basedir, '..', roledir), fn) + if os.path.exists(path): + return [path] + # if none of the above were found, just check the + # current filename against the basedir (this will already + # have ../files from runner, if it's a role task + path = utils.path_dwim(self.basedir, fn) + if os.path.exists(path): + return [path] + else: + if skip: + return [] + else: + return [None] + diff --git a/v2/ansible/plugins/lookup/flattened.py b/v2/ansible/plugins/lookup/flattened.py new file mode 100644 index 00000000000..831b2e91302 --- /dev/null +++ b/v2/ansible/plugins/lookup/flattened.py @@ -0,0 +1,78 @@ +# (c) 2013, Serge van Ginderachter +# +# 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 ansible.utils as utils +import ansible.errors as errors + + +def check_list_of_one_list(term): + # make sure term is not a list of one (list of one..) item + # return the final non list item if so + + if isinstance(term,list) and len(term) == 1: + term = term[0] + if isinstance(term,list): + term = check_list_of_one_list(term) + + return term + + + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + + def flatten(self, terms, inject): + + ret = [] + for term in terms: + term = check_list_of_one_list(term) + + if term == 'None' or term == 'null': + # ignore undefined items + break + + if isinstance(term, basestring): + # convert a variable to a list + term2 = utils.listify_lookup_plugin_terms(term, self.basedir, inject) + # but avoid converting a plain string to a list of one string + if term2 != [ term ]: + term = term2 + + if isinstance(term, list): + # if it's a list, check recursively for items that are a list + term = self.flatten(term, inject) + ret.extend(term) + else: + ret.append(term) + + return ret + + + def run(self, terms, inject=None, **kwargs): + + # see if the string represents a list and convert to list if so + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if not isinstance(terms, list): + raise errors.AnsibleError("with_flattened expects a list") + + ret = self.flatten(terms, inject) + return ret + diff --git a/v2/ansible/plugins/lookup/indexed_items.py b/v2/ansible/plugins/lookup/indexed_items.py new file mode 100644 index 00000000000..c1db1fdee2c --- /dev/null +++ b/v2/ansible/plugins/lookup/indexed_items.py @@ -0,0 +1,44 @@ +# (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 . + +from ansible.utils import safe_eval +import ansible.utils as utils +import ansible.errors as errors + +def flatten(terms): + ret = [] + for term in terms: + if isinstance(term, list): + ret.extend(term) + else: + ret.append(term) + return ret + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if not isinstance(terms, list): + raise errors.AnsibleError("with_indexed_items expects a list") + + items = flatten(terms) + return zip(range(len(items)), items) + diff --git a/v2/ansible/plugins/lookup/inventory_hostnames.py b/v2/ansible/plugins/lookup/inventory_hostnames.py new file mode 100644 index 00000000000..98523e13986 --- /dev/null +++ b/v2/ansible/plugins/lookup/inventory_hostnames.py @@ -0,0 +1,48 @@ +# (c) 2012, Michael DeHaan +# (c) 2013, Steven Dossett +# +# 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 . + +from ansible.utils import safe_eval +import ansible.utils as utils +import ansible.errors as errors +import ansible.inventory as inventory + +def flatten(terms): + ret = [] + for term in terms: + if isinstance(term, list): + ret.extend(term) + else: + ret.append(term) + return ret + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + if 'runner' in kwargs: + self.host_list = kwargs['runner'].inventory.host_list + else: + raise errors.AnsibleError("inventory_hostnames must be used as a loop. Example: \"with_inventory_hostnames: \'all\'\"") + + def run(self, terms, inject=None, **kwargs): + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if not isinstance(terms, list): + raise errors.AnsibleError("with_inventory_hostnames expects a list") + return flatten(inventory.Inventory(self.host_list).list_hosts(terms)) + diff --git a/v2/ansible/plugins/lookup/items.py b/v2/ansible/plugins/lookup/items.py new file mode 100644 index 00000000000..85e77d5380d --- /dev/null +++ b/v2/ansible/plugins/lookup/items.py @@ -0,0 +1,44 @@ +# (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 . + +from ansible.utils import safe_eval +import ansible.utils as utils +import ansible.errors as errors + +def flatten(terms): + ret = [] + for term in terms: + if isinstance(term, list): + ret.extend(term) + else: + ret.append(term) + return ret + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if not isinstance(terms, list) and not isinstance(terms,set): + raise errors.AnsibleError("with_items expects a list or a set") + + return flatten(terms) + + diff --git a/v2/ansible/plugins/lookup/lines.py b/v2/ansible/plugins/lookup/lines.py new file mode 100644 index 00000000000..5d4b70a8579 --- /dev/null +++ b/v2/ansible/plugins/lookup/lines.py @@ -0,0 +1,38 @@ +# (c) 2012, Daniel Hokka Zakrisson +# +# 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 subprocess +from ansible import utils, errors + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + ret = [] + for term in terms: + p = subprocess.Popen(term, cwd=self.basedir, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + (stdout, stderr) = p.communicate() + if p.returncode == 0: + ret.extend(stdout.splitlines()) + else: + raise errors.AnsibleError("lookup_plugin.lines(%s) returned %d" % (term, p.returncode)) + return ret diff --git a/v2/ansible/plugins/lookup/nested.py b/v2/ansible/plugins/lookup/nested.py new file mode 100644 index 00000000000..29c4a7d21cf --- /dev/null +++ b/v2/ansible/plugins/lookup/nested.py @@ -0,0 +1,73 @@ +# (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 ansible.utils as utils +from ansible.utils import safe_eval +import ansible.errors as errors + +def flatten(terms): + ret = [] + for term in terms: + if isinstance(term, list): + ret.extend(term) + elif isinstance(term, tuple): + ret.extend(term) + else: + ret.append(term) + return ret + +def combine(a,b): + results = [] + for x in a: + for y in b: + results.append(flatten([x,y])) + return results + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def __lookup_injects(self, terms, inject): + results = [] + for x in terms: + intermediate = utils.listify_lookup_plugin_terms(x, self.basedir, inject) + results.append(intermediate) + return results + + def run(self, terms, inject=None, **kwargs): + + # this code is common with 'items.py' consider moving to utils if we need it again + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + terms = self.__lookup_injects(terms, inject) + + my_list = terms[:] + my_list.reverse() + result = [] + if len(my_list) == 0: + raise errors.AnsibleError("with_nested requires at least one element in the nested list") + result = my_list.pop() + while len(my_list) > 0: + result2 = combine(result, my_list.pop()) + result = result2 + new_result = [] + for x in result: + new_result.append(flatten(x)) + return new_result + + diff --git a/v2/ansible/plugins/lookup/password.py b/v2/ansible/plugins/lookup/password.py new file mode 100644 index 00000000000..a066887e2c2 --- /dev/null +++ b/v2/ansible/plugins/lookup/password.py @@ -0,0 +1,129 @@ +# (c) 2012, Daniel Hokka Zakrisson +# (c) 2013, Javier Candeira +# (c) 2013, Maykel Moya +# +# 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 . + +from ansible import utils, errors +import os +import errno +from string import ascii_letters, digits +import string +import random + + +class LookupModule(object): + + LENGTH = 20 + + def __init__(self, length=None, encrypt=None, basedir=None, **kwargs): + self.basedir = basedir + + def random_salt(self): + salt_chars = ascii_letters + digits + './' + return utils.random_password(length=8, chars=salt_chars) + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + ret = [] + + for term in terms: + # you can't have escaped spaces in yor pathname + params = term.split() + relpath = params[0] + + paramvals = { + 'length': LookupModule.LENGTH, + 'encrypt': None, + 'chars': ['ascii_letters','digits',".,:-_"], + } + + # get non-default parameters if specified + try: + for param in params[1:]: + name, value = param.split('=') + assert(name in paramvals) + if name == 'length': + paramvals[name] = int(value) + elif name == 'chars': + use_chars=[] + if ",," in value: + use_chars.append(',') + use_chars.extend(value.replace(',,',',').split(',')) + paramvals['chars'] = use_chars + else: + paramvals[name] = value + except (ValueError, AssertionError), e: + raise errors.AnsibleError(e) + + length = paramvals['length'] + encrypt = paramvals['encrypt'] + use_chars = paramvals['chars'] + + # get password or create it if file doesn't exist + path = utils.path_dwim(self.basedir, relpath) + if not os.path.exists(path): + pathdir = os.path.dirname(path) + if not os.path.isdir(pathdir): + try: + os.makedirs(pathdir, mode=0700) + except OSError, e: + raise errors.AnsibleError("cannot create the path for the password lookup: %s (error was %s)" % (pathdir, str(e))) + + chars = "".join([getattr(string,c,c) for c in use_chars]).replace('"','').replace("'",'') + password = ''.join(random.choice(chars) for _ in range(length)) + + if encrypt is not None: + salt = self.random_salt() + content = '%s salt=%s' % (password, salt) + else: + content = password + with open(path, 'w') as f: + os.chmod(path, 0600) + f.write(content + '\n') + else: + content = open(path).read().rstrip() + sep = content.find(' ') + + if sep >= 0: + password = content[:sep] + salt = content[sep+1:].split('=')[1] + else: + password = content + salt = None + + # crypt requested, add salt if missing + if (encrypt is not None and not salt): + salt = self.random_salt() + content = '%s salt=%s' % (password, salt) + with open(path, 'w') as f: + os.chmod(path, 0600) + f.write(content + '\n') + # crypt not requested, remove salt if present + elif (encrypt is None and salt): + with open(path, 'w') as f: + os.chmod(path, 0600) + f.write(password + '\n') + + if encrypt: + password = utils.do_encrypt(password, encrypt, salt=salt) + + ret.append(password) + + return ret + diff --git a/v2/ansible/plugins/lookup/pipe.py b/v2/ansible/plugins/lookup/pipe.py new file mode 100644 index 00000000000..0cd9e1cda5d --- /dev/null +++ b/v2/ansible/plugins/lookup/pipe.py @@ -0,0 +1,52 @@ +# (c) 2012, Daniel Hokka Zakrisson +# +# 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 subprocess +from ansible import utils, errors + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if isinstance(terms, basestring): + terms = [ terms ] + + ret = [] + for term in terms: + ''' + http://docs.python.org/2/library/subprocess.html#popen-constructor + + The shell argument (which defaults to False) specifies whether to use the + shell as the program to execute. If shell is True, it is recommended to pass + args as a string rather than as a sequence + + https://github.com/ansible/ansible/issues/6550 + ''' + term = str(term) + + p = subprocess.Popen(term, cwd=self.basedir, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + (stdout, stderr) = p.communicate() + if p.returncode == 0: + ret.append(stdout.decode("utf-8").rstrip()) + else: + raise errors.AnsibleError("lookup_plugin.pipe(%s) returned %d" % (term, p.returncode)) + return ret diff --git a/v2/ansible/plugins/lookup/random_choice.py b/v2/ansible/plugins/lookup/random_choice.py new file mode 100644 index 00000000000..9b32c2f119b --- /dev/null +++ b/v2/ansible/plugins/lookup/random_choice.py @@ -0,0 +1,41 @@ +# (c) 2013, 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 random +from ansible import utils + +# useful for introducing chaos ... or just somewhat reasonably fair selection +# amongst available mirrors +# +# tasks: +# - debug: msg=$item +# with_random_choice: +# - one +# - two +# - three + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + return [ random.choice(terms) ] + diff --git a/v2/ansible/plugins/lookup/redis_kv.py b/v2/ansible/plugins/lookup/redis_kv.py new file mode 100644 index 00000000000..22c5c3754f7 --- /dev/null +++ b/v2/ansible/plugins/lookup/redis_kv.py @@ -0,0 +1,72 @@ +# (c) 2012, Jan-Piet Mens +# +# 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 . + +from ansible import utils, errors +import os +HAVE_REDIS=False +try: + import redis # https://github.com/andymccurdy/redis-py/ + HAVE_REDIS=True +except ImportError: + pass +import re + +# ============================================================== +# REDISGET: Obtain value from a GET on a Redis key. Terms +# expected: 0 = URL, 1 = Key +# URL may be empty, in which case redis://localhost:6379 assumed +# -------------------------------------------------------------- + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + if HAVE_REDIS == False: + raise errors.AnsibleError("Can't LOOKUP(redis_kv): module redis is not installed") + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + ret = [] + for term in terms: + (url,key) = term.split(',') + if url == "": + url = 'redis://localhost:6379' + + # urlsplit on Python 2.6.1 is broken. Hmm. Probably also the reason + # Redis' from_url() doesn't work here. + + p = '(?P[^:]+)://?(?P[^:/ ]+).?(?P[0-9]*).*' + + try: + m = re.search(p, url) + host = m.group('host') + port = int(m.group('port')) + except AttributeError: + raise errors.AnsibleError("Bad URI in redis lookup") + + try: + conn = redis.Redis(host=host, port=port) + res = conn.get(key) + if res is None: + res = "" + ret.append(res) + except: + ret.append("") # connection failed or key not found + return ret diff --git a/v2/ansible/plugins/lookup/sequence.py b/v2/ansible/plugins/lookup/sequence.py new file mode 100644 index 00000000000..b162b3069e7 --- /dev/null +++ b/v2/ansible/plugins/lookup/sequence.py @@ -0,0 +1,204 @@ +# (c) 2013, Jayson Vantuyl +# +# 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 . + +from ansible.errors import AnsibleError +import ansible.utils as utils +from re import compile as re_compile, IGNORECASE + +# shortcut format +NUM = "(0?x?[0-9a-f]+)" +SHORTCUT = re_compile( + "^(" + # Group 0 + NUM + # Group 1: Start + "-)?" + + NUM + # Group 2: End + "(/" + # Group 3 + NUM + # Group 4: Stride + ")?" + + "(:(.+))?$", # Group 5, Group 6: Format String + IGNORECASE +) + + +class LookupModule(object): + """ + sequence lookup module + + Used to generate some sequence of items. Takes arguments in two forms. + + The simple / shortcut form is: + + [start-]end[/stride][:format] + + As indicated by the brackets: start, stride, and format string are all + optional. The format string is in the style of printf. This can be used + to pad with zeros, format in hexadecimal, etc. All of the numerical values + can be specified in octal (i.e. 0664) or hexadecimal (i.e. 0x3f8). + Negative numbers are not supported. + + Some examples: + + 5 -> ["1","2","3","4","5"] + 5-8 -> ["5", "6", "7", "8"] + 2-10/2 -> ["2", "4", "6", "8", "10"] + 4:host%02d -> ["host01","host02","host03","host04"] + + The standard Ansible key-value form is accepted as well. For example: + + start=5 end=11 stride=2 format=0x%02x -> ["0x05","0x07","0x09","0x0a"] + + This format takes an alternate form of "end" called "count", which counts + some number from the starting value. For example: + + count=5 -> ["1", "2", "3", "4", "5"] + start=0x0f00 count=4 format=%04x -> ["0f00", "0f01", "0f02", "0f03"] + start=0 count=5 stride=2 -> ["0", "2", "4", "6", "8"] + start=1 count=5 stride=2 -> ["1", "3", "5", "7", "9"] + + The count option is mostly useful for avoiding off-by-one errors and errors + calculating the number of entries in a sequence when a stride is specified. + """ + + def __init__(self, basedir, **kwargs): + """absorb any keyword args""" + self.basedir = basedir + + def reset(self): + """set sensible defaults""" + self.start = 1 + self.count = None + self.end = None + self.stride = 1 + self.format = "%d" + + def parse_kv_args(self, args): + """parse key-value style arguments""" + for arg in ["start", "end", "count", "stride"]: + try: + arg_raw = args.pop(arg, None) + if arg_raw is None: + continue + arg_cooked = int(arg_raw, 0) + setattr(self, arg, arg_cooked) + except ValueError: + raise AnsibleError( + "can't parse arg %s=%r as integer" + % (arg, arg_raw) + ) + if 'format' in args: + self.format = args.pop("format") + if args: + raise AnsibleError( + "unrecognized arguments to with_sequence: %r" + % args.keys() + ) + + def parse_simple_args(self, term): + """parse the shortcut forms, return True/False""" + match = SHORTCUT.match(term) + if not match: + return False + + _, start, end, _, stride, _, format = match.groups() + + if start is not None: + try: + start = int(start, 0) + except ValueError: + raise AnsibleError("can't parse start=%s as integer" % start) + if end is not None: + try: + end = int(end, 0) + except ValueError: + raise AnsibleError("can't parse end=%s as integer" % end) + if stride is not None: + try: + stride = int(stride, 0) + except ValueError: + raise AnsibleError("can't parse stride=%s as integer" % stride) + + if start is not None: + self.start = start + if end is not None: + self.end = end + if stride is not None: + self.stride = stride + if format is not None: + self.format = format + + def sanity_check(self): + if self.count is None and self.end is None: + raise AnsibleError( + "must specify count or end in with_sequence" + ) + elif self.count is not None and self.end is not None: + raise AnsibleError( + "can't specify both count and end in with_sequence" + ) + elif self.count is not None: + # convert count to end + self.end = self.start + self.count * self.stride - 1 + del self.count + if self.end < self.start: + raise AnsibleError("can't count backwards") + if self.format.count('%') != 1: + raise AnsibleError("bad formatting string: %s" % self.format) + + def generate_sequence(self): + numbers = xrange(self.start, self.end + 1, self.stride) + + for i in numbers: + try: + formatted = self.format % i + yield formatted + except (ValueError, TypeError): + raise AnsibleError( + "problem formatting %r with %r" % self.format + ) + + def run(self, terms, inject=None, **kwargs): + results = [] + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + if isinstance(terms, basestring): + terms = [ terms ] + + for term in terms: + try: + self.reset() # clear out things for this iteration + + try: + if not self.parse_simple_args(term): + self.parse_kv_args(utils.parse_kv(term)) + except Exception: + raise AnsibleError( + "unknown error parsing with_sequence arguments: %r" + % term + ) + + self.sanity_check() + + results.extend(self.generate_sequence()) + except AnsibleError: + raise + except Exception: + raise AnsibleError( + "unknown error generating sequence" + ) + + return results diff --git a/v2/ansible/plugins/lookup/subelements.py b/v2/ansible/plugins/lookup/subelements.py new file mode 100644 index 00000000000..f33aae717d1 --- /dev/null +++ b/v2/ansible/plugins/lookup/subelements.py @@ -0,0 +1,67 @@ +# (c) 2013, Serge van Ginderachter +# +# 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 ansible.utils as utils +import ansible.errors as errors + + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + + def run(self, terms, inject=None, **kwargs): + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + terms[0] = utils.listify_lookup_plugin_terms(terms[0], self.basedir, inject) + + if not isinstance(terms, list) or not len(terms) == 2: + raise errors.AnsibleError( + "subelements lookup expects a list of two items, first a dict or a list, and second a string") + terms[0] = utils.listify_lookup_plugin_terms(terms[0], self.basedir, inject) + if not isinstance(terms[0], (list, dict)) or not isinstance(terms[1], basestring): + raise errors.AnsibleError( + "subelements lookup expects a list of two items, first a dict or a list, and second a string") + + if isinstance(terms[0], dict): # convert to list: + if terms[0].get('skipped',False) != False: + # the registered result was completely skipped + return [] + elementlist = [] + for key in terms[0].iterkeys(): + elementlist.append(terms[0][key]) + else: + elementlist = terms[0] + subelement = terms[1] + + ret = [] + for item0 in elementlist: + if not isinstance(item0, dict): + raise errors.AnsibleError("subelements lookup expects a dictionary, got '%s'" %item0) + if item0.get('skipped',False) != False: + # this particular item is to be skipped + continue + if not subelement in item0: + raise errors.AnsibleError("could not find '%s' key in iterated item '%s'" % (subelement, item0)) + if not isinstance(item0[subelement], list): + raise errors.AnsibleError("the key %s should point to a list, got '%s'" % (subelement, item0[subelement])) + sublist = item0.pop(subelement, []) + for item1 in sublist: + ret.append((item0, item1)) + + return ret + diff --git a/v2/ansible/plugins/lookup/template.py b/v2/ansible/plugins/lookup/template.py new file mode 100644 index 00000000000..e009b6b76b9 --- /dev/null +++ b/v2/ansible/plugins/lookup/template.py @@ -0,0 +1,33 @@ +# (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 . + +from ansible.utils import template +import ansible.utils as utils + +class LookupModule(object): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, inject=None, **kwargs): + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + + ret = [] + for term in terms: + ret.append(template.template_from_file(self.basedir, term, inject)) + return ret diff --git a/v2/ansible/plugins/lookup/together.py b/v2/ansible/plugins/lookup/together.py new file mode 100644 index 00000000000..07332c9fb97 --- /dev/null +++ b/v2/ansible/plugins/lookup/together.py @@ -0,0 +1,64 @@ +# (c) 2013, Bradley Young +# +# 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 ansible.utils as utils +from ansible.utils import safe_eval +import ansible.errors as errors +from itertools import izip_longest + +def flatten(terms): + ret = [] + for term in terms: + if isinstance(term, list): + ret.extend(term) + elif isinstance(term, tuple): + ret.extend(term) + else: + ret.append(term) + return ret + +class LookupModule(object): + """ + Transpose a list of arrays: + [1, 2, 3], [4, 5, 6] -> [1, 4], [2, 5], [3, 6] + Replace any empty spots in 2nd array with None: + [1, 2], [3] -> [1, 3], [2, None] + """ + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def __lookup_injects(self, terms, inject): + results = [] + for x in terms: + intermediate = utils.listify_lookup_plugin_terms(x, self.basedir, inject) + results.append(intermediate) + return results + + def run(self, terms, inject=None, **kwargs): + + # this code is common with 'items.py' consider moving to utils if we need it again + + terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) + terms = self.__lookup_injects(terms, inject) + + my_list = terms[:] + if len(my_list) == 0: + raise errors.AnsibleError("with_together requires at least one element in each list") + return [flatten(x) for x in izip_longest(*my_list, fillvalue=None)] + + diff --git a/v2/test/errors/test_errors.py b/v2/test/errors/test_errors.py index 5b24dc4345d..30ff4111288 100644 --- a/v2/test/errors/test_errors.py +++ b/v2/test/errors/test_errors.py @@ -51,7 +51,7 @@ class TestErrors(unittest.TestCase): mock_method.return_value = ('this is line 1\n', '') e = AnsibleError(self.message, self.obj) - self.assertEqual(e.message, "This is the error message\nThe error appears to have been in 'foo.yml': line 1, column 1,\nbut may actually be before there depending on the exact syntax problem.\n\nthis is line 1\n^\n") + self.assertEqual(e.message, "This is the error message\n\nThe error appears to have been in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nthis is line 1\n^\n") def test_get_error_lines_from_file(self): m = mock_open() @@ -63,12 +63,12 @@ class TestErrors(unittest.TestCase): self.obj._line_number = 1 self.obj._column_number = 1 e = AnsibleError(self.message, self.obj) - self.assertEqual(e.message, "This is the error message\nThe error appears to have been in 'foo.yml': line 1, column 1,\nbut may actually be before there depending on the exact syntax problem.\n\nthis is line 1\n^\n") + self.assertEqual(e.message, "This is the error message\n\nThe error appears to have been in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nthis is line 1\n^\n") # this line will not be found, as it is out of the index range self.obj._data_source = 'foo.yml' self.obj._line_number = 2 self.obj._column_number = 1 e = AnsibleError(self.message, self.obj) - self.assertEqual(e.message, "This is the error message\nThe error appears to have been in 'foo.yml': line 2, column 1,\nbut may actually be before there depending on the exact syntax problem.\n\n(specified line no longer in file, maybe it changed?)") + self.assertEqual(e.message, "This is the error message\n\nThe error appears to have been in 'foo.yml': line 2, column 1, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\n(specified line no longer in file, maybe it changed?)") diff --git a/v2/test/parsing/test_mod_args.py b/v2/test/parsing/test_mod_args.py index 0f9ee28decb..187edfa03cb 100644 --- a/v2/test/parsing/test_mod_args.py +++ b/v2/test/parsing/test_mod_args.py @@ -31,7 +31,6 @@ class TestModArgsDwim(unittest.TestCase): # and the task knows the line numbers def setUp(self): - self.m = ModuleArgsParser() pass def _debug(self, mod, args, to): @@ -43,7 +42,8 @@ class TestModArgsDwim(unittest.TestCase): pass def test_basic_shell(self): - mod, args, to = self.m.parse(dict(shell='echo hi')) + m = ModuleArgsParser(dict(shell='echo hi')) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'command') self.assertEqual(args, dict( @@ -53,7 +53,8 @@ class TestModArgsDwim(unittest.TestCase): self.assertIsNone(to) def test_basic_command(self): - mod, args, to = self.m.parse(dict(command='echo hi')) + m = ModuleArgsParser(dict(command='echo hi')) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'command') self.assertEqual(args, dict( @@ -62,7 +63,8 @@ class TestModArgsDwim(unittest.TestCase): self.assertIsNone(to) def test_shell_with_modifiers(self): - mod, args, to = self.m.parse(dict(shell='/bin/foo creates=/tmp/baz removes=/tmp/bleep')) + m = ModuleArgsParser(dict(shell='/bin/foo creates=/tmp/baz removes=/tmp/bleep')) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'command') self.assertEqual(args, dict( @@ -74,42 +76,55 @@ class TestModArgsDwim(unittest.TestCase): self.assertIsNone(to) def test_normal_usage(self): - mod, args, to = self.m.parse(dict(copy='src=a dest=b')) + m = ModuleArgsParser(dict(copy='src=a dest=b')) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'copy') self.assertEqual(args, dict(src='a', dest='b')) self.assertIsNone(to) def test_complex_args(self): - mod, args, to = self.m.parse(dict(copy=dict(src='a', dest='b'))) + m = ModuleArgsParser(dict(copy=dict(src='a', dest='b'))) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'copy') self.assertEqual(args, dict(src='a', dest='b')) self.assertIsNone(to) def test_action_with_complex(self): - mod, args, to = self.m.parse(dict(action=dict(module='copy', src='a', dest='b'))) + m = ModuleArgsParser(dict(action=dict(module='copy', src='a', dest='b'))) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'copy') self.assertEqual(args, dict(src='a', dest='b')) self.assertIsNone(to) def test_action_with_complex_and_complex_args(self): - mod, args, to = self.m.parse(dict(action=dict(module='copy', args=dict(src='a', dest='b')))) + m = ModuleArgsParser(dict(action=dict(module='copy', args=dict(src='a', dest='b')))) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'copy') self.assertEqual(args, dict(src='a', dest='b')) self.assertIsNone(to) def test_local_action_string(self): - mod, args, to = self.m.parse(dict(local_action='copy src=a dest=b')) + m = ModuleArgsParser(dict(local_action='copy src=a dest=b')) + mod, args, to = m.parse() self._debug(mod, args, to) self.assertEqual(mod, 'copy') self.assertEqual(args, dict(src='a', dest='b')) self.assertIs(to, 'localhost') def test_multiple_actions(self): - self.assertRaises(AnsibleParserError, self.m.parse, dict(action='shell echo hi', local_action='shell echo hi')) - self.assertRaises(AnsibleParserError, self.m.parse, dict(action='shell echo hi', shell='echo hi')) - self.assertRaises(AnsibleParserError, self.m.parse, dict(local_action='shell echo hi', shell='echo hi')) - self.assertRaises(AnsibleParserError, self.m.parse, dict(ping='data=hi', shell='echo hi')) + m = ModuleArgsParser(dict(action='shell echo hi', local_action='shell echo hi')) + self.assertRaises(AnsibleParserError, m.parse) + + m = ModuleArgsParser(dict(action='shell echo hi', shell='echo hi')) + self.assertRaises(AnsibleParserError, m.parse) + + m = ModuleArgsParser(dict(local_action='shell echo hi', shell='echo hi')) + self.assertRaises(AnsibleParserError, m.parse) + + m = ModuleArgsParser(dict(ping='data=hi', shell='echo hi')) + self.assertRaises(AnsibleParserError, m.parse) + diff --git a/v2/test/playbook/test_playbook.py b/v2/test/playbook/test_playbook.py new file mode 100644 index 00000000000..640057820e8 --- /dev/null +++ b/v2/test/playbook/test_playbook.py @@ -0,0 +1,65 @@ +# (c) 2012-2014, 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock + +from ansible.errors import AnsibleError, AnsibleParserError +from ansible.playbook import Playbook + +from test.mock.loader import DictDataLoader + +class TestPlaybook(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_empty_playbook(self): + p = Playbook() + + def test_basic_playbook(self): + fake_loader = DictDataLoader({ + "test_file.yml":""" + - hosts: all + """, + }) + p = Playbook.load("test_file.yml", loader=fake_loader) + + def test_bad_playbook_files(self): + fake_loader = DictDataLoader({ + # represents a playbook which is not a list of plays + "bad_list.yml": """ + foo: bar + + """, + # represents a playbook where a play entry is mis-formatted + "bad_entry.yml": """ + - + - "This should be a mapping..." + + """, + }) + self.assertRaises(AnsibleParserError, Playbook.load, "bad_list.yml", fake_loader) + self.assertRaises(AnsibleParserError, Playbook.load, "bad_entry.yml", fake_loader) + diff --git a/v2/test/playbook/test_task_include.py b/v2/test/playbook/test_task_include.py new file mode 100644 index 00000000000..42a63b72049 --- /dev/null +++ b/v2/test/playbook/test_task_include.py @@ -0,0 +1,63 @@ +# (c) 2012-2014, 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests import unittest +from ansible.errors import AnsibleParserError +from ansible.parsing.yaml.objects import AnsibleMapping +from ansible.playbook.task_include import TaskInclude + +from test.mock.loader import DictDataLoader + +class TestTaskInclude(unittest.TestCase): + + def setUp(self): + self._fake_loader = DictDataLoader({ + "foo.yml": """ + - shell: echo "hello world" + """ + }) + + pass + + def tearDown(self): + pass + + def test_empty_task_include(self): + ti = TaskInclude() + + def test_basic_task_include(self): + ti = TaskInclude.load(AnsibleMapping(include='foo.yml'), loader=self._fake_loader) + + def test_task_include_with_loop(self): + ti = TaskInclude.load(AnsibleMapping(include='foo.yml', with_items=['a', 'b', 'c']), loader=self._fake_loader) + + def test_task_include_with_conditional(self): + ti = TaskInclude.load(AnsibleMapping(include='foo.yml', when="1 == 1"), loader=self._fake_loader) + + def test_task_include_with_tags(self): + ti = TaskInclude.load(AnsibleMapping(include='foo.yml', tags="foo"), loader=self._fake_loader) + ti = TaskInclude.load(AnsibleMapping(include='foo.yml', tags=["foo", "bar"]), loader=self._fake_loader) + + def test_task_include_errors(self): + self.assertRaises(AnsibleParserError, TaskInclude.load, AnsibleMapping(include=''), loader=self._fake_loader) + self.assertRaises(AnsibleParserError, TaskInclude.load, AnsibleMapping(include='foo.yml', vars="1"), loader=self._fake_loader) + self.assertRaises(AnsibleParserError, TaskInclude.load, AnsibleMapping(include='foo.yml a=1', vars=dict(b=2)), loader=self._fake_loader) + diff --git a/v2/test/plugins/test_plugins.py b/v2/test/plugins/test_plugins.py index e6bef809e6a..0d0fe400d0e 100644 --- a/v2/test/plugins/test_plugins.py +++ b/v2/test/plugins/test_plugins.py @@ -36,10 +36,6 @@ class TestErrors(unittest.TestCase): def tearDown(self): pass - def test_push_basedir(self): - push_basedir('/root/foo/bar') - self.assertEqual(_basedirs, ['/root/foo/bar']) - @patch.object(PluginLoader, '_get_paths') def test_print_paths(self, mock_method): mock_method.return_value = ['/path/one', '/path/two', '/path/three']