1# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17
18# Make coding more python3-ish
19from __future__ import (absolute_import, division, print_function)
20__metaclass__ = type
21
22import os
23
24from ansible import constants as C
25from ansible.errors import AnsibleError
26from ansible.module_utils._text import to_text
27from ansible.playbook.task_include import TaskInclude
28from ansible.playbook.role_include import IncludeRole
29from ansible.template import Templar
30from ansible.utils.display import Display
31
32display = Display()
33
34
35class IncludedFile:
36
37    def __init__(self, filename, args, vars, task, is_role=False):
38        self._filename = filename
39        self._args = args
40        self._vars = vars
41        self._task = task
42        self._hosts = []
43        self._is_role = is_role
44
45    def add_host(self, host):
46        if host not in self._hosts:
47            self._hosts.append(host)
48            return
49        raise ValueError()
50
51    def __eq__(self, other):
52        return (other._filename == self._filename and
53                other._args == self._args and
54                other._vars == self._vars and
55                other._task._uuid == self._task._uuid and
56                other._task._parent._uuid == self._task._parent._uuid)
57
58    def __repr__(self):
59        return "%s (args=%s vars=%s): %s" % (self._filename, self._args, self._vars, self._hosts)
60
61    @staticmethod
62    def process_include_results(results, iterator, loader, variable_manager):
63        included_files = []
64        task_vars_cache = {}
65
66        for res in results:
67
68            original_host = res._host
69            original_task = res._task
70
71            if original_task.action in C._ACTION_ALL_INCLUDES:
72                if original_task.loop:
73                    if 'results' not in res._result:
74                        continue
75                    include_results = res._result['results']
76                else:
77                    include_results = [res._result]
78
79                for include_result in include_results:
80                    # if the task result was skipped or failed, continue
81                    if 'skipped' in include_result and include_result['skipped'] or 'failed' in include_result and include_result['failed']:
82                        continue
83
84                    cache_key = (iterator._play, original_host, original_task)
85                    try:
86                        task_vars = task_vars_cache[cache_key]
87                    except KeyError:
88                        task_vars = task_vars_cache[cache_key] = variable_manager.get_vars(play=iterator._play, host=original_host, task=original_task)
89
90                    include_args = include_result.get('include_args', dict())
91                    special_vars = {}
92                    loop_var = include_result.get('ansible_loop_var', 'item')
93                    index_var = include_result.get('ansible_index_var')
94                    if loop_var in include_result:
95                        task_vars[loop_var] = special_vars[loop_var] = include_result[loop_var]
96                    if index_var and index_var in include_result:
97                        task_vars[index_var] = special_vars[index_var] = include_result[index_var]
98                    if '_ansible_item_label' in include_result:
99                        task_vars['_ansible_item_label'] = special_vars['_ansible_item_label'] = include_result['_ansible_item_label']
100                    if 'ansible_loop' in include_result:
101                        task_vars['ansible_loop'] = special_vars['ansible_loop'] = include_result['ansible_loop']
102                    if original_task.no_log and '_ansible_no_log' not in include_args:
103                        task_vars['_ansible_no_log'] = special_vars['_ansible_no_log'] = original_task.no_log
104
105                    # get search path for this task to pass to lookup plugins that may be used in pathing to
106                    # the included file
107                    task_vars['ansible_search_path'] = original_task.get_search_path()
108
109                    # ensure basedir is always in (dwim already searches here but we need to display it)
110                    if loader.get_basedir() not in task_vars['ansible_search_path']:
111                        task_vars['ansible_search_path'].append(loader.get_basedir())
112
113                    templar = Templar(loader=loader, variables=task_vars)
114
115                    if original_task.action in C._ACTION_ALL_INCLUDE_TASKS:
116                        include_file = None
117                        if original_task:
118                            if original_task.static:
119                                continue
120
121                            if original_task._parent:
122                                # handle relative includes by walking up the list of parent include
123                                # tasks and checking the relative result to see if it exists
124                                parent_include = original_task._parent
125                                cumulative_path = None
126                                while parent_include is not None:
127                                    if not isinstance(parent_include, TaskInclude):
128                                        parent_include = parent_include._parent
129                                        continue
130                                    if isinstance(parent_include, IncludeRole):
131                                        parent_include_dir = parent_include._role_path
132                                    else:
133                                        try:
134                                            parent_include_dir = os.path.dirname(templar.template(parent_include.args.get('_raw_params')))
135                                        except AnsibleError as e:
136                                            parent_include_dir = ''
137                                            display.warning(
138                                                'Templating the path of the parent %s failed. The path to the '
139                                                'included file may not be found. '
140                                                'The error was: %s.' % (original_task.action, to_text(e))
141                                            )
142                                    if cumulative_path is not None and not os.path.isabs(cumulative_path):
143                                        cumulative_path = os.path.join(parent_include_dir, cumulative_path)
144                                    else:
145                                        cumulative_path = parent_include_dir
146                                    include_target = templar.template(include_result['include'])
147                                    if original_task._role:
148                                        new_basedir = os.path.join(original_task._role._role_path, 'tasks', cumulative_path)
149                                        candidates = [loader.path_dwim_relative(original_task._role._role_path, 'tasks', include_target),
150                                                      loader.path_dwim_relative(new_basedir, 'tasks', include_target)]
151                                        for include_file in candidates:
152                                            try:
153                                                # may throw OSError
154                                                os.stat(include_file)
155                                                # or select the task file if it exists
156                                                break
157                                            except OSError:
158                                                pass
159                                    else:
160                                        include_file = loader.path_dwim_relative(loader.get_basedir(), cumulative_path, include_target)
161
162                                    if os.path.exists(include_file):
163                                        break
164                                    else:
165                                        parent_include = parent_include._parent
166
167                        if include_file is None:
168                            if original_task._role:
169                                include_target = templar.template(include_result['include'])
170                                include_file = loader.path_dwim_relative(original_task._role._role_path, 'tasks', include_target)
171                            else:
172                                include_file = loader.path_dwim(include_result['include'])
173
174                        include_file = templar.template(include_file)
175                        inc_file = IncludedFile(include_file, include_args, special_vars, original_task)
176                    else:
177                        # template the included role's name here
178                        role_name = include_args.pop('name', include_args.pop('role', None))
179                        if role_name is not None:
180                            role_name = templar.template(role_name)
181
182                        new_task = original_task.copy()
183                        new_task._role_name = role_name
184                        for from_arg in new_task.FROM_ARGS:
185                            if from_arg in include_args:
186                                from_key = from_arg.replace('_from', '')
187                                new_task._from_files[from_key] = templar.template(include_args.pop(from_arg))
188
189                        inc_file = IncludedFile(role_name, include_args, special_vars, new_task, is_role=True)
190
191                    idx = 0
192                    orig_inc_file = inc_file
193                    while 1:
194                        try:
195                            pos = included_files[idx:].index(orig_inc_file)
196                            # pos is relative to idx since we are slicing
197                            # use idx + pos due to relative indexing
198                            inc_file = included_files[idx + pos]
199                        except ValueError:
200                            included_files.append(orig_inc_file)
201                            inc_file = orig_inc_file
202
203                        try:
204                            inc_file.add_host(original_host)
205                        except ValueError:
206                            # The host already exists for this include, advance forward, this is a new include
207                            idx += pos + 1
208                        else:
209                            break
210
211        return included_files
212