Browse code

include_role (role revamp implementation) (#17232)

* 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

Brian Coca authored on 2016/08/27 02:42:13
Showing 11 changed files
... ...
@@ -39,6 +39,7 @@ Ansible Changes By Release
39 39
 - ipmi
40 40
   * ipmi_boot
41 41
   * ipmi_power
42
+- include_role
42 43
 - letsencrypt
43 44
 - logicmonitor
44 45
 - logicmonitor_facts
... ...
@@ -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:
... ...
@@ -1 +1 @@
1
-Subproject commit 4912ec30a71b09577a78f940c6a772b38b76f1a2
1
+Subproject commit 91a839f1e3de58be8b981d3278aa5dc8ff59c508
... ...
@@ -1 +1 @@
1
-Subproject commit f29efb56264a9ad95b97765e367ef5b7915ab877
1
+Subproject commit 1aeb9f8a8c6d54663bbad6db385f568c04182ec6
... ...
@@ -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