From 71e774e8bf024a947d7ceb410197c7476c2b21c4 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Mon, 26 Jan 2015 11:29:59 -0600 Subject: [PATCH] Implementing playbook-level includes and getting includes integration test working --- v2/ansible/playbook/__init__.py | 6 +- v2/ansible/playbook/playbook_include.py | 95 ++++++++++++++++++++++- v2/ansible/playbook/task_include.py | 2 - v2/ansible/plugins/strategies/__init__.py | 4 + 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/v2/ansible/playbook/__init__.py b/v2/ansible/playbook/__init__.py index 8ecdadc878..30a8262abc 100644 --- a/v2/ansible/playbook/__init__.py +++ b/v2/ansible/playbook/__init__.py @@ -72,11 +72,11 @@ class Playbook: 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, variable_manager=variable_manager, loader=self._loader) + pb = PlaybookInclude.load(entry, variable_manager=variable_manager, loader=self._loader) + self._entries.extend(pb._entries) else: entry_obj = Play.load(entry, variable_manager=variable_manager, loader=self._loader) - - self._entries.append(entry_obj) + self._entries.append(entry_obj) def get_loader(self): return self._loader diff --git a/v2/ansible/playbook/playbook_include.py b/v2/ansible/playbook/playbook_include.py index 2eb1c17e4f..7512e3693c 100644 --- a/v2/ansible/playbook/playbook_include.py +++ b/v2/ansible/playbook/playbook_include.py @@ -19,7 +19,100 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +from ansible.parsing.splitter import split_args, parse_kv +from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping +from ansible.playbook.attribute import FieldAttribute from ansible.playbook.base import Base +from ansible.playbook.conditional import Conditional +from ansible.playbook.taggable import Taggable class PlaybookInclude(Base): - pass + + _name = FieldAttribute(isa='string') + _include = FieldAttribute(isa='string') + _vars = FieldAttribute(isa='dict', default=dict()) + + @staticmethod + def load(data, variable_manager=None, loader=None): + return PlaybookInclude().load_data(ds=data, variable_manager=variable_manager, loader=loader) + + def load_data(self, ds, variable_manager=None, loader=None): + ''' + Overrides the base load_data(), as we're actually going to return a new + Playbook() object rather than a PlaybookInclude object + ''' + + # import here to avoid a dependency loop + from ansible.playbook import Playbook + + # first, we use the original parent method to correctly load the object + # via the munge/load_data system we normally use for other playbook objects + new_obj = super(PlaybookInclude, self).load_data(ds, variable_manager, loader) + + # then we use the object to load a Playbook + pb = Playbook(loader=loader) + pb._load_playbook_data(file_name=new_obj.include, variable_manager=variable_manager) + + # finally, playbook includes can specify a list of variables, which are simply + # used to update the vars of each play in the playbook + for entry in pb._entries: + entry.vars.update(new_obj.vars) + + return pb + + def munge(self, ds): + ''' + Regorganizes the data for a PlaybookInclude 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_loader: + 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 + diff --git a/v2/ansible/playbook/task_include.py b/v2/ansible/playbook/task_include.py index f48fc2f72a..d7aba9e815 100644 --- a/v2/ansible/playbook/task_include.py +++ b/v2/ansible/playbook/task_include.py @@ -55,9 +55,7 @@ class TaskInclude(Base, Conditional, Taggable): _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, use_handlers=False): self._block = block diff --git a/v2/ansible/plugins/strategies/__init__.py b/v2/ansible/plugins/strategies/__init__.py index 9a64216cef..1e25a84d10 100644 --- a/v2/ansible/plugins/strategies/__init__.py +++ b/v2/ansible/plugins/strategies/__init__.py @@ -169,6 +169,10 @@ class StrategyBase: elif result[0] == 'notify_handler': host = result[1] handler_name = result[2] + + if handler_name not in self._notified_handlers: + self._notified_handlers[handler_name] = [] + if host not in self._notified_handlers[handler_name]: self._notified_handlers[handler_name].append(host)