* attempt #11 to role_include
* fixes from jimi-c
* do not override load_data, move all to load
* removed debugging
* implemented tasks_from parameter, must break cache
* fixed issue with cache and tasks_from
* make resolution of from_tasks prioritize literal
* avoid role dependency dedupe when include_role
* fixed role deps and handlers are now loaded
* simplified code, enabled k=v parsing
used example from jimi-c
* load role defaults for task when include_role
* fixed issue with from_Tasks overriding all subdirs
* corrected priority order of main candidates
* made tasks_from a more generic interface to roles
* fix block inheritance and handler order
* allow vars: clause into included role
* pull vars already processed vs from raw data
* fix from jimi-c blocks i broke
* added back append for dynamic includes
* only allow for basename in from parameter
* fix for docs when no default
* fixed notes
* added include_role to changelog
... | ... |
@@ -283,13 +283,15 @@ class DocCLI(CLI): |
283 | 283 |
choices = '' |
284 | 284 |
if 'choices' in opt: |
285 | 285 |
choices = "(Choices: " + ", ".join(str(i) for i in opt['choices']) + ")" |
286 |
+ default = '' |
|
286 | 287 |
if 'default' in opt or not required: |
287 | 288 |
default = "[Default: " + str(opt.get('default', '(null)')) + "]" |
288 | 289 |
text.append(textwrap.fill(CLI.tty_ify(choices + default), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) |
289 | 290 |
|
290 | 291 |
if 'notes' in doc and doc['notes'] and len(doc['notes']) > 0: |
291 |
- notes = " ".join(doc['notes']) |
|
292 |
- text.append("Notes:%s\n" % textwrap.fill(CLI.tty_ify(notes), limit-6, initial_indent=" ", subsequent_indent=opt_indent)) |
|
292 |
+ text.append("Notes:") |
|
293 |
+ for note in doc['notes']: |
|
294 |
+ text.append(textwrap.fill(CLI.tty_ify(note), limit-6, initial_indent=" * ", subsequent_indent=opt_indent)) |
|
293 | 295 |
|
294 | 296 |
if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0: |
295 | 297 |
req = ", ".join(doc['requirements']) |
... | ... |
@@ -404,6 +404,15 @@ class TaskExecutor: |
404 | 404 |
include_file = templar.template(include_file) |
405 | 405 |
return dict(include=include_file, include_variables=include_variables) |
406 | 406 |
|
407 |
+ #TODO: not needed? |
|
408 |
+ # if this task is a IncludeRole, we just return now with a success code so the main thread can expand the task list for the given host |
|
409 |
+ elif self._task.action == 'include_role': |
|
410 |
+ include_variables = self._task.args.copy() |
|
411 |
+ role = include_variables.pop('name') |
|
412 |
+ if not role: |
|
413 |
+ return dict(failed=True, msg="No role was specified to include") |
|
414 |
+ return dict(name=role, include_variables=include_variables) |
|
415 |
+ |
|
407 | 416 |
# Now we do final validation on the task, which sets all fields to their final values. |
408 | 417 |
self._task.post_validate(templar=templar) |
409 | 418 |
if '_variable_params' in self._task.args: |
... | ... |
@@ -264,7 +264,6 @@ class ModuleArgsParser: |
264 | 264 |
if 'action' in self._task_ds: |
265 | 265 |
# an old school 'action' statement |
266 | 266 |
thing = self._task_ds['action'] |
267 |
- action, args = self._normalize_parameters(thing, additional_args=additional_args) |
|
268 | 267 |
|
269 | 268 |
# local_action |
270 | 269 |
if 'local_action' in self._task_ds: |
... | ... |
@@ -273,19 +272,20 @@ class ModuleArgsParser: |
273 | 273 |
raise AnsibleParserError("action and local_action are mutually exclusive", obj=self._task_ds) |
274 | 274 |
thing = self._task_ds.get('local_action', '') |
275 | 275 |
delegate_to = 'localhost' |
276 |
- action, args = self._normalize_parameters(thing, additional_args=additional_args) |
|
277 | 276 |
|
278 | 277 |
# module: <stuff> is the more new-style invocation |
279 | 278 |
|
280 | 279 |
# walk the input dictionary to see we recognize a module name |
281 | 280 |
for (item, value) in iteritems(self._task_ds): |
282 |
- if item in module_loader or item == 'meta' or item == 'include': |
|
281 |
+ if item in module_loader or item in ['meta', 'include', 'include_role']: |
|
283 | 282 |
# finding more than one module name is a problem |
284 | 283 |
if action is not None: |
285 | 284 |
raise AnsibleParserError("conflicting action statements", obj=self._task_ds) |
286 | 285 |
action = item |
287 | 286 |
thing = value |
288 |
- action, args = self._normalize_parameters(value, action=action, additional_args=additional_args) |
|
287 |
+ #TODO: find out if we should break here? Currently last matching action, break would make it first one |
|
288 |
+ |
|
289 |
+ action, args = self._normalize_parameters(thing, action=action, additional_args=additional_args) |
|
289 | 290 |
|
290 | 291 |
# if we didn't see any module in the task at all, it's not a task really |
291 | 292 |
if action is None: |
... | ... |
@@ -22,8 +22,7 @@ import os |
22 | 22 |
|
23 | 23 |
from ansible import constants as C |
24 | 24 |
from ansible.compat.six import string_types |
25 |
-from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound |
|
26 |
-from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleSequence |
|
25 |
+from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound, AnsibleError |
|
27 | 26 |
|
28 | 27 |
try: |
29 | 28 |
from __main__ import display |
... | ... |
@@ -81,6 +80,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h |
81 | 81 |
from ansible.playbook.handler import Handler |
82 | 82 |
from ansible.playbook.task import Task |
83 | 83 |
from ansible.playbook.task_include import TaskInclude |
84 |
+ from ansible.playbook.role_include import IncludeRole |
|
84 | 85 |
from ansible.playbook.handler_task_include import HandlerTaskInclude |
85 | 86 |
from ansible.template import Templar |
86 | 87 |
|
... | ... |
@@ -172,7 +172,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h |
172 | 172 |
if not found: |
173 | 173 |
try: |
174 | 174 |
include_target = templar.template(t.args['_raw_params']) |
175 |
- except AnsibleUndefinedVariable as e: |
|
175 |
+ except AnsibleUndefinedVariable: |
|
176 | 176 |
raise AnsibleParserError( |
177 | 177 |
"Error when evaluating variable in include name: %s.\n\n" \ |
178 | 178 |
"When using static includes, ensure that any variables used in their names are defined in vars/vars_files\n" \ |
... | ... |
@@ -198,7 +198,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h |
198 | 198 |
# because the recursive nature of helper methods means we may be loading |
199 | 199 |
# nested includes, and we want the include order printed correctly |
200 | 200 |
display.display("statically included: %s" % include_file, color=C.COLOR_SKIP) |
201 |
- except AnsibleFileNotFound as e: |
|
201 |
+ except AnsibleFileNotFound: |
|
202 | 202 |
if t.static or \ |
203 | 203 |
C.DEFAULT_TASK_INCLUDES_STATIC or \ |
204 | 204 |
C.DEFAULT_HANDLER_INCLUDES_STATIC and use_handlers: |
... | ... |
@@ -258,11 +258,24 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h |
258 | 258 |
task_list.extend(included_blocks) |
259 | 259 |
else: |
260 | 260 |
task_list.append(t) |
261 |
+ |
|
262 |
+ elif 'include_role' in task_ds: |
|
263 |
+ task_list.extend( |
|
264 |
+ IncludeRole.load( |
|
265 |
+ task_ds, |
|
266 |
+ block=block, |
|
267 |
+ role=role, |
|
268 |
+ task_include=None, |
|
269 |
+ variable_manager=variable_manager, |
|
270 |
+ loader=loader |
|
271 |
+ ) |
|
272 |
+ ) |
|
261 | 273 |
else: |
262 | 274 |
if use_handlers: |
263 | 275 |
t = Handler.load(task_ds, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader) |
264 | 276 |
else: |
265 | 277 |
t = Task.load(task_ds, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader) |
278 |
+ |
|
266 | 279 |
task_list.append(t) |
267 | 280 |
|
268 | 281 |
return task_list |
... | ... |
@@ -66,7 +66,7 @@ class Role(Base, Become, Conditional, Taggable): |
66 | 66 |
_delegate_to = FieldAttribute(isa='string') |
67 | 67 |
_delegate_facts = FieldAttribute(isa='bool', default=False) |
68 | 68 |
|
69 |
- def __init__(self, play=None): |
|
69 |
+ def __init__(self, play=None, from_files=None): |
|
70 | 70 |
self._role_name = None |
71 | 71 |
self._role_path = None |
72 | 72 |
self._role_params = dict() |
... | ... |
@@ -83,6 +83,10 @@ class Role(Base, Become, Conditional, Taggable): |
83 | 83 |
self._had_task_run = dict() |
84 | 84 |
self._completed = dict() |
85 | 85 |
|
86 |
+ if from_files is None: |
|
87 |
+ from_files = {} |
|
88 |
+ self._tasks_from = from_files.get('tasks') |
|
89 |
+ |
|
86 | 90 |
super(Role, self).__init__() |
87 | 91 |
|
88 | 92 |
def __repr__(self): |
... | ... |
@@ -92,7 +96,10 @@ class Role(Base, Become, Conditional, Taggable): |
92 | 92 |
return self._role_name |
93 | 93 |
|
94 | 94 |
@staticmethod |
95 |
- def load(role_include, play, parent_role=None): |
|
95 |
+ def load(role_include, play, parent_role=None, from_files=None): |
|
96 |
+ |
|
97 |
+ if from_files is None: |
|
98 |
+ from_files = {} |
|
96 | 99 |
try: |
97 | 100 |
# The ROLE_CACHE is a dictionary of role names, with each entry |
98 | 101 |
# containing another dictionary corresponding to a set of parameters |
... | ... |
@@ -104,6 +111,10 @@ class Role(Base, Become, Conditional, Taggable): |
104 | 104 |
params['when'] = role_include.when |
105 | 105 |
if role_include.tags is not None: |
106 | 106 |
params['tags'] = role_include.tags |
107 |
+ if from_files is not None: |
|
108 |
+ params['from_files'] = from_files |
|
109 |
+ if role_include.vars: |
|
110 |
+ params['vars'] = role_include.vars |
|
107 | 111 |
hashed_params = hash_params(params) |
108 | 112 |
if role_include.role in play.ROLE_CACHE: |
109 | 113 |
for (entry, role_obj) in iteritems(play.ROLE_CACHE[role_include.role]): |
... | ... |
@@ -112,7 +123,7 @@ class Role(Base, Become, Conditional, Taggable): |
112 | 112 |
role_obj.add_parent(parent_role) |
113 | 113 |
return role_obj |
114 | 114 |
|
115 |
- r = Role(play=play) |
|
115 |
+ r = Role(play=play, from_files=from_files) |
|
116 | 116 |
r._load_role_data(role_include, parent_role=parent_role) |
117 | 117 |
|
118 | 118 |
if role_include.role not in play.ROLE_CACHE: |
... | ... |
@@ -163,7 +174,7 @@ class Role(Base, Become, Conditional, Taggable): |
163 | 163 |
else: |
164 | 164 |
self._metadata = RoleMetadata() |
165 | 165 |
|
166 |
- task_data = self._load_role_yaml('tasks') |
|
166 |
+ task_data = self._load_role_yaml('tasks', main=self._tasks_from) |
|
167 | 167 |
if task_data: |
168 | 168 |
try: |
169 | 169 |
self._task_blocks = load_list_of_blocks(task_data, play=self._play, role=self, loader=self._loader, variable_manager=self._variable_manager) |
... | ... |
@@ -190,23 +201,36 @@ class Role(Base, Become, Conditional, Taggable): |
190 | 190 |
elif not isinstance(self._default_vars, dict): |
191 | 191 |
raise AnsibleParserError("The defaults/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) |
192 | 192 |
|
193 |
- def _load_role_yaml(self, subdir): |
|
193 |
+ def _load_role_yaml(self, subdir, main=None): |
|
194 | 194 |
file_path = os.path.join(self._role_path, subdir) |
195 | 195 |
if self._loader.path_exists(file_path) and self._loader.is_directory(file_path): |
196 |
- main_file = self._resolve_main(file_path) |
|
196 |
+ main_file = self._resolve_main(file_path, main) |
|
197 | 197 |
if self._loader.path_exists(main_file): |
198 | 198 |
return self._loader.load_from_file(main_file) |
199 | 199 |
return None |
200 | 200 |
|
201 |
- def _resolve_main(self, basepath): |
|
201 |
+ def _resolve_main(self, basepath, main=None): |
|
202 | 202 |
''' flexibly handle variations in main filenames ''' |
203 |
+ |
|
204 |
+ post = False |
|
205 |
+ # allow override if set, otherwise use default |
|
206 |
+ if main is None: |
|
207 |
+ main = 'main' |
|
208 |
+ post = True |
|
209 |
+ |
|
210 |
+ bare_main = os.path.join(basepath, main) |
|
211 |
+ |
|
203 | 212 |
possible_mains = ( |
204 |
- os.path.join(basepath, 'main.yml'), |
|
205 |
- os.path.join(basepath, 'main.yaml'), |
|
206 |
- os.path.join(basepath, 'main.json'), |
|
207 |
- os.path.join(basepath, 'main'), |
|
213 |
+ os.path.join(basepath, '%s.yml' % main), |
|
214 |
+ os.path.join(basepath, '%s.yaml' % main), |
|
215 |
+ os.path.join(basepath, '%s.json' % main), |
|
208 | 216 |
) |
209 | 217 |
|
218 |
+ if post: |
|
219 |
+ possible_mains = possible_mains + (bare_main,) |
|
220 |
+ else: |
|
221 |
+ possible_mains = (bare_main,) + possible_mains |
|
222 |
+ |
|
210 | 223 |
if sum([self._loader.is_file(x) for x in possible_mains]) > 1: |
211 | 224 |
raise AnsibleError("found multiple main files at %s, only one allowed" % (basepath)) |
212 | 225 |
else: |
... | ... |
@@ -274,6 +298,7 @@ class Role(Base, Become, Conditional, Taggable): |
274 | 274 |
for dep in self.get_all_dependencies(): |
275 | 275 |
all_vars = combine_vars(all_vars, dep.get_vars(include_params=include_params)) |
276 | 276 |
|
277 |
+ all_vars = combine_vars(all_vars, self.vars) |
|
277 | 278 |
all_vars = combine_vars(all_vars, self._role_vars) |
278 | 279 |
if include_params: |
279 | 280 |
all_vars = combine_vars(all_vars, self.get_role_params(dep_chain=dep_chain)) |
280 | 281 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,82 @@ |
0 |
+# Copyright (c) 2012 Red Hat, Inc. All rights reserved. |
|
1 |
+# |
|
2 |
+# This file is part of Ansible |
|
3 |
+# |
|
4 |
+# Ansible is free software: you can redistribute it and/or modify |
|
5 |
+# it under the terms of the GNU General Public License as published by |
|
6 |
+# the Free Software Foundation, either version 3 of the License, or |
|
7 |
+# (at your option) any later version. |
|
8 |
+# |
|
9 |
+# Ansible is distributed in the hope that it will be useful, |
|
10 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
+# GNU General Public License for more details. |
|
13 |
+# |
|
14 |
+# You should have received a copy of the GNU General Public License |
|
15 |
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>. |
|
16 |
+ |
|
17 |
+# Make coding more python3-ish |
|
18 |
+from __future__ import (absolute_import, division, print_function) |
|
19 |
+__metaclass__ = type |
|
20 |
+ |
|
21 |
+from os.path import basename |
|
22 |
+ |
|
23 |
+from ansible.playbook.attribute import FieldAttribute |
|
24 |
+from ansible.playbook.task import Task |
|
25 |
+from ansible.playbook.role import Role |
|
26 |
+from ansible.playbook.role.include import RoleInclude |
|
27 |
+ |
|
28 |
+try: |
|
29 |
+ from __main__ import display |
|
30 |
+except ImportError: |
|
31 |
+ from ansible.utils.display import Display |
|
32 |
+ display = Display() |
|
33 |
+ |
|
34 |
+__all__ = ['IncludeRole'] |
|
35 |
+ |
|
36 |
+ |
|
37 |
+class IncludeRole(Task): |
|
38 |
+ |
|
39 |
+ """ |
|
40 |
+ A Role include is derived from a regular role to handle the special |
|
41 |
+ circumstances related to the `- include_role: ...` |
|
42 |
+ """ |
|
43 |
+ |
|
44 |
+ # ================================================================================= |
|
45 |
+ # ATTRIBUTES |
|
46 |
+ |
|
47 |
+ _name = FieldAttribute(isa='string', default=None) |
|
48 |
+ _tasks_from = FieldAttribute(isa='string', default=None) |
|
49 |
+ |
|
50 |
+ # these should not be changeable? |
|
51 |
+ _static = FieldAttribute(isa='bool', default=False) |
|
52 |
+ _private = FieldAttribute(isa='bool', default=True) |
|
53 |
+ |
|
54 |
+ @staticmethod |
|
55 |
+ def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): |
|
56 |
+ |
|
57 |
+ r = IncludeRole().load_data(data, variable_manager=variable_manager, loader=loader) |
|
58 |
+ args = r.preprocess_data(data).get('args', dict()) |
|
59 |
+ |
|
60 |
+ ri = RoleInclude.load(args.get('name'), play=block._play, variable_manager=variable_manager, loader=loader) |
|
61 |
+ ri.vars.update(r.vars) |
|
62 |
+ |
|
63 |
+ # build options for roles |
|
64 |
+ from_files = {} |
|
65 |
+ if args.get('tasks_from'): |
|
66 |
+ from_files['tasks'] = basename(args.get('tasks_from')) |
|
67 |
+ |
|
68 |
+ #build role |
|
69 |
+ actual_role = Role.load(ri, block._play, parent_role=role, from_files=from_files) |
|
70 |
+ |
|
71 |
+ # compile role |
|
72 |
+ blocks = actual_role.compile(play=block._play) |
|
73 |
+ |
|
74 |
+ # set parent to ensure proper inheritance |
|
75 |
+ for b in blocks: |
|
76 |
+ b._parent = block |
|
77 |
+ |
|
78 |
+ # updated available handlers in play |
|
79 |
+ block._play.handlers = block._play.handlers + actual_role.get_handler_blocks(play=block._play) |
|
80 |
+ |
|
81 |
+ return blocks |
... | ... |
@@ -40,6 +40,7 @@ from ansible.module_utils.facts import Facts |
40 | 40 |
from ansible.playbook.helpers import load_list_of_blocks |
41 | 41 |
from ansible.playbook.included_file import IncludedFile |
42 | 42 |
from ansible.playbook.task_include import TaskInclude |
43 |
+from ansible.playbook.role_include import IncludeRole |
|
43 | 44 |
from ansible.plugins import action_loader, connection_loader, filter_loader, lookup_loader, module_loader, test_loader |
44 | 45 |
from ansible.template import Templar |
45 | 46 |
from ansible.utils.unicode import to_unicode |
... | ... |
@@ -258,7 +259,7 @@ class StrategyBase: |
258 | 258 |
|
259 | 259 |
def parent_handler_match(target_handler, handler_name): |
260 | 260 |
if target_handler: |
261 |
- if isinstance(target_handler, TaskInclude): |
|
261 |
+ if isinstance(target_handler, (TaskInclude, IncludeRole)): |
|
262 | 262 |
try: |
263 | 263 |
handler_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, task=target_handler) |
264 | 264 |
templar = Templar(loader=self._loader, variables=handler_vars) |
... | ... |
@@ -477,7 +478,7 @@ class StrategyBase: |
477 | 477 |
|
478 | 478 |
# If this is a role task, mark the parent role as being run (if |
479 | 479 |
# the task was ok or failed, but not skipped or unreachable) |
480 |
- if original_task._role is not None and role_ran: |
|
480 |
+ if original_task._role is not None and role_ran and original_task.action != 'include_role': |
|
481 | 481 |
# lookup the role in the ROLE_CACHE to make sure we're dealing |
482 | 482 |
# with the correct object and mark it as executed |
483 | 483 |
for (entry, role_obj) in iteritems(iterator._play.ROLE_CACHE[original_task._role._role_name]): |
... | ... |
@@ -232,11 +232,11 @@ class VariableManager: |
232 | 232 |
for role in play.get_roles(): |
233 | 233 |
all_vars = combine_vars(all_vars, role.get_default_vars()) |
234 | 234 |
|
235 |
- # if we have a task in this context, and that task has a role, make |
|
236 |
- # sure it sees its defaults above any other roles, as we previously |
|
237 |
- # (v1) made sure each task had a copy of its roles default vars |
|
238 |
- if task and task._role is not None: |
|
239 |
- all_vars = combine_vars(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain())) |
|
235 |
+ # if we have a task in this context, and that task has a role, make |
|
236 |
+ # sure it sees its defaults above any other roles, as we previously |
|
237 |
+ # (v1) made sure each task had a copy of its roles default vars |
|
238 |
+ if task and task._role is not None and (play or task.action == 'include_role'): |
|
239 |
+ all_vars = combine_vars(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain())) |
|
240 | 240 |
|
241 | 241 |
if host: |
242 | 242 |
# next, if a host is specified, we load any vars from group_vars |