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 fnmatch
23
24from ansible import constants as C
25from ansible.module_utils.six import iteritems
26from ansible.module_utils.parsing.convert_bool import boolean
27from ansible.playbook.block import Block
28from ansible.playbook.task import Task
29from ansible.utils.display import Display
30
31
32display = Display()
33
34
35__all__ = ['PlayIterator']
36
37
38class HostState:
39    def __init__(self, blocks):
40        self._blocks = blocks[:]
41
42        self.cur_block = 0
43        self.cur_regular_task = 0
44        self.cur_rescue_task = 0
45        self.cur_always_task = 0
46        self.cur_dep_chain = None
47        self.run_state = PlayIterator.ITERATING_SETUP
48        self.fail_state = PlayIterator.FAILED_NONE
49        self.pending_setup = False
50        self.tasks_child_state = None
51        self.rescue_child_state = None
52        self.always_child_state = None
53        self.did_rescue = False
54        self.did_start_at_task = False
55
56    def __repr__(self):
57        return "HostState(%r)" % self._blocks
58
59    def __str__(self):
60        def _run_state_to_string(n):
61            states = ["ITERATING_SETUP", "ITERATING_TASKS", "ITERATING_RESCUE", "ITERATING_ALWAYS", "ITERATING_COMPLETE"]
62            try:
63                return states[n]
64            except IndexError:
65                return "UNKNOWN STATE"
66
67        def _failed_state_to_string(n):
68            states = {1: "FAILED_SETUP", 2: "FAILED_TASKS", 4: "FAILED_RESCUE", 8: "FAILED_ALWAYS"}
69            if n == 0:
70                return "FAILED_NONE"
71            else:
72                ret = []
73                for i in (1, 2, 4, 8):
74                    if n & i:
75                        ret.append(states[i])
76                return "|".join(ret)
77
78        return ("HOST STATE: block=%d, task=%d, rescue=%d, always=%d, run_state=%s, fail_state=%s, pending_setup=%s, tasks child state? (%s), "
79                "rescue child state? (%s), always child state? (%s), did rescue? %s, did start at task? %s" % (
80                    self.cur_block,
81                    self.cur_regular_task,
82                    self.cur_rescue_task,
83                    self.cur_always_task,
84                    _run_state_to_string(self.run_state),
85                    _failed_state_to_string(self.fail_state),
86                    self.pending_setup,
87                    self.tasks_child_state,
88                    self.rescue_child_state,
89                    self.always_child_state,
90                    self.did_rescue,
91                    self.did_start_at_task,
92                ))
93
94    def __eq__(self, other):
95        if not isinstance(other, HostState):
96            return False
97
98        for attr in ('_blocks', 'cur_block', 'cur_regular_task', 'cur_rescue_task', 'cur_always_task',
99                     'run_state', 'fail_state', 'pending_setup', 'cur_dep_chain',
100                     'tasks_child_state', 'rescue_child_state', 'always_child_state'):
101            if getattr(self, attr) != getattr(other, attr):
102                return False
103
104        return True
105
106    def get_current_block(self):
107        return self._blocks[self.cur_block]
108
109    def copy(self):
110        new_state = HostState(self._blocks)
111        new_state.cur_block = self.cur_block
112        new_state.cur_regular_task = self.cur_regular_task
113        new_state.cur_rescue_task = self.cur_rescue_task
114        new_state.cur_always_task = self.cur_always_task
115        new_state.run_state = self.run_state
116        new_state.fail_state = self.fail_state
117        new_state.pending_setup = self.pending_setup
118        new_state.did_rescue = self.did_rescue
119        new_state.did_start_at_task = self.did_start_at_task
120        if self.cur_dep_chain is not None:
121            new_state.cur_dep_chain = self.cur_dep_chain[:]
122        if self.tasks_child_state is not None:
123            new_state.tasks_child_state = self.tasks_child_state.copy()
124        if self.rescue_child_state is not None:
125            new_state.rescue_child_state = self.rescue_child_state.copy()
126        if self.always_child_state is not None:
127            new_state.always_child_state = self.always_child_state.copy()
128        return new_state
129
130
131class PlayIterator:
132
133    # the primary running states for the play iteration
134    ITERATING_SETUP = 0
135    ITERATING_TASKS = 1
136    ITERATING_RESCUE = 2
137    ITERATING_ALWAYS = 3
138    ITERATING_COMPLETE = 4
139
140    # the failure states for the play iteration, which are powers
141    # of 2 as they may be or'ed together in certain circumstances
142    FAILED_NONE = 0
143    FAILED_SETUP = 1
144    FAILED_TASKS = 2
145    FAILED_RESCUE = 4
146    FAILED_ALWAYS = 8
147
148    def __init__(self, inventory, play, play_context, variable_manager, all_vars, start_at_done=False):
149        self._play = play
150        self._blocks = []
151        self._variable_manager = variable_manager
152
153        # Default options to gather
154        gather_subset = self._play.gather_subset
155        gather_timeout = self._play.gather_timeout
156        fact_path = self._play.fact_path
157
158        setup_block = Block(play=self._play)
159        # Gathering facts with run_once would copy the facts from one host to
160        # the others.
161        setup_block.run_once = False
162        setup_task = Task(block=setup_block)
163        setup_task.action = 'gather_facts'
164        setup_task.name = 'Gathering Facts'
165        setup_task.args = {
166            'gather_subset': gather_subset,
167        }
168
169        # Unless play is specifically tagged, gathering should 'always' run
170        if not self._play.tags:
171            setup_task.tags = ['always']
172
173        if gather_timeout:
174            setup_task.args['gather_timeout'] = gather_timeout
175        if fact_path:
176            setup_task.args['fact_path'] = fact_path
177        setup_task.set_loader(self._play._loader)
178        # short circuit fact gathering if the entire playbook is conditional
179        if self._play._included_conditional is not None:
180            setup_task.when = self._play._included_conditional[:]
181        setup_block.block = [setup_task]
182
183        setup_block = setup_block.filter_tagged_tasks(all_vars)
184        self._blocks.append(setup_block)
185
186        for block in self._play.compile():
187            new_block = block.filter_tagged_tasks(all_vars)
188            if new_block.has_tasks():
189                self._blocks.append(new_block)
190
191        self._host_states = {}
192        start_at_matched = False
193        batch = inventory.get_hosts(self._play.hosts, order=self._play.order)
194        self.batch_size = len(batch)
195        for host in batch:
196            self._host_states[host.name] = HostState(blocks=self._blocks)
197            # if we're looking to start at a specific task, iterate through
198            # the tasks for this host until we find the specified task
199            if play_context.start_at_task is not None and not start_at_done:
200                while True:
201                    (s, task) = self.get_next_task_for_host(host, peek=True)
202                    if s.run_state == self.ITERATING_COMPLETE:
203                        break
204                    if task.name == play_context.start_at_task or (task.name and fnmatch.fnmatch(task.name, play_context.start_at_task)) or \
205                       task.get_name() == play_context.start_at_task or fnmatch.fnmatch(task.get_name(), play_context.start_at_task):
206                        start_at_matched = True
207                        break
208                    else:
209                        self.get_next_task_for_host(host)
210
211                # finally, reset the host's state to ITERATING_SETUP
212                if start_at_matched:
213                    self._host_states[host.name].did_start_at_task = True
214                    self._host_states[host.name].run_state = self.ITERATING_SETUP
215
216        if start_at_matched:
217            # we have our match, so clear the start_at_task field on the
218            # play context to flag that we've started at a task (and future
219            # plays won't try to advance)
220            play_context.start_at_task = None
221
222    def get_host_state(self, host):
223        # Since we're using the PlayIterator to carry forward failed hosts,
224        # in the event that a previous host was not in the current inventory
225        # we create a stub state for it now
226        if host.name not in self._host_states:
227            self._host_states[host.name] = HostState(blocks=[])
228
229        return self._host_states[host.name].copy()
230
231    def cache_block_tasks(self, block):
232        # now a noop, we've changed the way we do caching and finding of
233        # original task entries, but just in case any 3rd party strategies
234        # are using this we're leaving it here for now
235        return
236
237    def get_next_task_for_host(self, host, peek=False):
238
239        display.debug("getting the next task for host %s" % host.name)
240        s = self.get_host_state(host)
241
242        task = None
243        if s.run_state == self.ITERATING_COMPLETE:
244            display.debug("host %s is done iterating, returning" % host.name)
245            return (s, None)
246
247        (s, task) = self._get_next_task_from_state(s, host=host)
248
249        if not peek:
250            self._host_states[host.name] = s
251
252        display.debug("done getting next task for host %s" % host.name)
253        display.debug(" ^ task is: %s" % task)
254        display.debug(" ^ state is: %s" % s)
255        return (s, task)
256
257    def _get_next_task_from_state(self, state, host):
258
259        task = None
260
261        # try and find the next task, given the current state.
262        while True:
263            # try to get the current block from the list of blocks, and
264            # if we run past the end of the list we know we're done with
265            # this block
266            try:
267                block = state._blocks[state.cur_block]
268            except IndexError:
269                state.run_state = self.ITERATING_COMPLETE
270                return (state, None)
271
272            if state.run_state == self.ITERATING_SETUP:
273                # First, we check to see if we were pending setup. If not, this is
274                # the first trip through ITERATING_SETUP, so we set the pending_setup
275                # flag and try to determine if we do in fact want to gather facts for
276                # the specified host.
277                if not state.pending_setup:
278                    state.pending_setup = True
279
280                    # Gather facts if the default is 'smart' and we have not yet
281                    # done it for this host; or if 'explicit' and the play sets
282                    # gather_facts to True; or if 'implicit' and the play does
283                    # NOT explicitly set gather_facts to False.
284
285                    gathering = C.DEFAULT_GATHERING
286                    implied = self._play.gather_facts is None or boolean(self._play.gather_facts, strict=False)
287
288                    if (gathering == 'implicit' and implied) or \
289                       (gathering == 'explicit' and boolean(self._play.gather_facts, strict=False)) or \
290                       (gathering == 'smart' and implied and not (self._variable_manager._fact_cache.get(host.name, {}).get('_ansible_facts_gathered', False))):
291                        # The setup block is always self._blocks[0], as we inject it
292                        # during the play compilation in __init__ above.
293                        setup_block = self._blocks[0]
294                        if setup_block.has_tasks() and len(setup_block.block) > 0:
295                            task = setup_block.block[0]
296                else:
297                    # This is the second trip through ITERATING_SETUP, so we clear
298                    # the flag and move onto the next block in the list while setting
299                    # the run state to ITERATING_TASKS
300                    state.pending_setup = False
301
302                    state.run_state = self.ITERATING_TASKS
303                    if not state.did_start_at_task:
304                        state.cur_block += 1
305                        state.cur_regular_task = 0
306                        state.cur_rescue_task = 0
307                        state.cur_always_task = 0
308                        state.tasks_child_state = None
309                        state.rescue_child_state = None
310                        state.always_child_state = None
311
312            elif state.run_state == self.ITERATING_TASKS:
313                # clear the pending setup flag, since we're past that and it didn't fail
314                if state.pending_setup:
315                    state.pending_setup = False
316
317                # First, we check for a child task state that is not failed, and if we
318                # have one recurse into it for the next task. If we're done with the child
319                # state, we clear it and drop back to getting the next task from the list.
320                if state.tasks_child_state:
321                    (state.tasks_child_state, task) = self._get_next_task_from_state(state.tasks_child_state, host=host)
322                    if self._check_failed_state(state.tasks_child_state):
323                        # failed child state, so clear it and move into the rescue portion
324                        state.tasks_child_state = None
325                        self._set_failed_state(state)
326                    else:
327                        # get the next task recursively
328                        if task is None or state.tasks_child_state.run_state == self.ITERATING_COMPLETE:
329                            # we're done with the child state, so clear it and continue
330                            # back to the top of the loop to get the next task
331                            state.tasks_child_state = None
332                            continue
333                else:
334                    # First here, we check to see if we've failed anywhere down the chain
335                    # of states we have, and if so we move onto the rescue portion. Otherwise,
336                    # we check to see if we've moved past the end of the list of tasks. If so,
337                    # we move into the always portion of the block, otherwise we get the next
338                    # task from the list.
339                    if self._check_failed_state(state):
340                        state.run_state = self.ITERATING_RESCUE
341                    elif state.cur_regular_task >= len(block.block):
342                        state.run_state = self.ITERATING_ALWAYS
343                    else:
344                        task = block.block[state.cur_regular_task]
345                        # if the current task is actually a child block, create a child
346                        # state for us to recurse into on the next pass
347                        if isinstance(task, Block):
348                            state.tasks_child_state = HostState(blocks=[task])
349                            state.tasks_child_state.run_state = self.ITERATING_TASKS
350                            # since we've created the child state, clear the task
351                            # so we can pick up the child state on the next pass
352                            task = None
353                        state.cur_regular_task += 1
354
355            elif state.run_state == self.ITERATING_RESCUE:
356                # The process here is identical to ITERATING_TASKS, except instead
357                # we move into the always portion of the block.
358                if host.name in self._play._removed_hosts:
359                    self._play._removed_hosts.remove(host.name)
360
361                if state.rescue_child_state:
362                    (state.rescue_child_state, task) = self._get_next_task_from_state(state.rescue_child_state, host=host)
363                    if self._check_failed_state(state.rescue_child_state):
364                        state.rescue_child_state = None
365                        self._set_failed_state(state)
366                    else:
367                        if task is None or state.rescue_child_state.run_state == self.ITERATING_COMPLETE:
368                            state.rescue_child_state = None
369                            continue
370                else:
371                    if state.fail_state & self.FAILED_RESCUE == self.FAILED_RESCUE:
372                        state.run_state = self.ITERATING_ALWAYS
373                    elif state.cur_rescue_task >= len(block.rescue):
374                        if len(block.rescue) > 0:
375                            state.fail_state = self.FAILED_NONE
376                        state.run_state = self.ITERATING_ALWAYS
377                        state.did_rescue = True
378                    else:
379                        task = block.rescue[state.cur_rescue_task]
380                        if isinstance(task, Block):
381                            state.rescue_child_state = HostState(blocks=[task])
382                            state.rescue_child_state.run_state = self.ITERATING_TASKS
383                            task = None
384                        state.cur_rescue_task += 1
385
386            elif state.run_state == self.ITERATING_ALWAYS:
387                # And again, the process here is identical to ITERATING_TASKS, except
388                # instead we either move onto the next block in the list, or we set the
389                # run state to ITERATING_COMPLETE in the event of any errors, or when we
390                # have hit the end of the list of blocks.
391                if state.always_child_state:
392                    (state.always_child_state, task) = self._get_next_task_from_state(state.always_child_state, host=host)
393                    if self._check_failed_state(state.always_child_state):
394                        state.always_child_state = None
395                        self._set_failed_state(state)
396                    else:
397                        if task is None or state.always_child_state.run_state == self.ITERATING_COMPLETE:
398                            state.always_child_state = None
399                            continue
400                else:
401                    if state.cur_always_task >= len(block.always):
402                        if state.fail_state != self.FAILED_NONE:
403                            state.run_state = self.ITERATING_COMPLETE
404                        else:
405                            state.cur_block += 1
406                            state.cur_regular_task = 0
407                            state.cur_rescue_task = 0
408                            state.cur_always_task = 0
409                            state.run_state = self.ITERATING_TASKS
410                            state.tasks_child_state = None
411                            state.rescue_child_state = None
412                            state.always_child_state = None
413                            state.did_rescue = False
414                    else:
415                        task = block.always[state.cur_always_task]
416                        if isinstance(task, Block):
417                            state.always_child_state = HostState(blocks=[task])
418                            state.always_child_state.run_state = self.ITERATING_TASKS
419                            task = None
420                        state.cur_always_task += 1
421
422            elif state.run_state == self.ITERATING_COMPLETE:
423                return (state, None)
424
425            # if something above set the task, break out of the loop now
426            if task:
427                break
428
429        return (state, task)
430
431    def _set_failed_state(self, state):
432        if state.run_state == self.ITERATING_SETUP:
433            state.fail_state |= self.FAILED_SETUP
434            state.run_state = self.ITERATING_COMPLETE
435        elif state.run_state == self.ITERATING_TASKS:
436            if state.tasks_child_state is not None:
437                state.tasks_child_state = self._set_failed_state(state.tasks_child_state)
438            else:
439                state.fail_state |= self.FAILED_TASKS
440                if state._blocks[state.cur_block].rescue:
441                    state.run_state = self.ITERATING_RESCUE
442                elif state._blocks[state.cur_block].always:
443                    state.run_state = self.ITERATING_ALWAYS
444                else:
445                    state.run_state = self.ITERATING_COMPLETE
446        elif state.run_state == self.ITERATING_RESCUE:
447            if state.rescue_child_state is not None:
448                state.rescue_child_state = self._set_failed_state(state.rescue_child_state)
449            else:
450                state.fail_state |= self.FAILED_RESCUE
451                if state._blocks[state.cur_block].always:
452                    state.run_state = self.ITERATING_ALWAYS
453                else:
454                    state.run_state = self.ITERATING_COMPLETE
455        elif state.run_state == self.ITERATING_ALWAYS:
456            if state.always_child_state is not None:
457                state.always_child_state = self._set_failed_state(state.always_child_state)
458            else:
459                state.fail_state |= self.FAILED_ALWAYS
460                state.run_state = self.ITERATING_COMPLETE
461        return state
462
463    def mark_host_failed(self, host):
464        s = self.get_host_state(host)
465        display.debug("marking host %s failed, current state: %s" % (host, s))
466        s = self._set_failed_state(s)
467        display.debug("^ failed state is now: %s" % s)
468        self._host_states[host.name] = s
469        self._play._removed_hosts.append(host.name)
470
471    def get_failed_hosts(self):
472        return dict((host, True) for (host, state) in iteritems(self._host_states) if self._check_failed_state(state))
473
474    def _check_failed_state(self, state):
475        if state is None:
476            return False
477        elif state.run_state == self.ITERATING_RESCUE and self._check_failed_state(state.rescue_child_state):
478            return True
479        elif state.run_state == self.ITERATING_ALWAYS and self._check_failed_state(state.always_child_state):
480            return True
481        elif state.fail_state != self.FAILED_NONE:
482            if state.run_state == self.ITERATING_RESCUE and state.fail_state & self.FAILED_RESCUE == 0:
483                return False
484            elif state.run_state == self.ITERATING_ALWAYS and state.fail_state & self.FAILED_ALWAYS == 0:
485                return False
486            else:
487                return not (state.did_rescue and state.fail_state & self.FAILED_ALWAYS == 0)
488        elif state.run_state == self.ITERATING_TASKS and self._check_failed_state(state.tasks_child_state):
489            cur_block = state._blocks[state.cur_block]
490            if len(cur_block.rescue) > 0 and state.fail_state & self.FAILED_RESCUE == 0:
491                return False
492            else:
493                return True
494        return False
495
496    def is_failed(self, host):
497        s = self.get_host_state(host)
498        return self._check_failed_state(s)
499
500    def get_active_state(self, state):
501        '''
502        Finds the active state, recursively if necessary when there are child states.
503        '''
504        if state.run_state == self.ITERATING_TASKS and state.tasks_child_state is not None:
505            return self.get_active_state(state.tasks_child_state)
506        elif state.run_state == self.ITERATING_RESCUE and state.rescue_child_state is not None:
507            return self.get_active_state(state.rescue_child_state)
508        elif state.run_state == self.ITERATING_ALWAYS and state.always_child_state is not None:
509            return self.get_active_state(state.always_child_state)
510        return state
511
512    def is_any_block_rescuing(self, state):
513        '''
514        Given the current HostState state, determines if the current block, or any child blocks,
515        are in rescue mode.
516        '''
517        if state.run_state == self.ITERATING_RESCUE:
518            return True
519        if state.tasks_child_state is not None:
520            return self.is_any_block_rescuing(state.tasks_child_state)
521        return False
522
523    def get_original_task(self, host, task):
524        # now a noop because we've changed the way we do caching
525        return (None, None)
526
527    def _insert_tasks_into_state(self, state, task_list):
528        # if we've failed at all, or if the task list is empty, just return the current state
529        if state.fail_state != self.FAILED_NONE and state.run_state not in (self.ITERATING_RESCUE, self.ITERATING_ALWAYS) or not task_list:
530            return state
531
532        if state.run_state == self.ITERATING_TASKS:
533            if state.tasks_child_state:
534                state.tasks_child_state = self._insert_tasks_into_state(state.tasks_child_state, task_list)
535            else:
536                target_block = state._blocks[state.cur_block].copy()
537                before = target_block.block[:state.cur_regular_task]
538                after = target_block.block[state.cur_regular_task:]
539                target_block.block = before + task_list + after
540                state._blocks[state.cur_block] = target_block
541        elif state.run_state == self.ITERATING_RESCUE:
542            if state.rescue_child_state:
543                state.rescue_child_state = self._insert_tasks_into_state(state.rescue_child_state, task_list)
544            else:
545                target_block = state._blocks[state.cur_block].copy()
546                before = target_block.rescue[:state.cur_rescue_task]
547                after = target_block.rescue[state.cur_rescue_task:]
548                target_block.rescue = before + task_list + after
549                state._blocks[state.cur_block] = target_block
550        elif state.run_state == self.ITERATING_ALWAYS:
551            if state.always_child_state:
552                state.always_child_state = self._insert_tasks_into_state(state.always_child_state, task_list)
553            else:
554                target_block = state._blocks[state.cur_block].copy()
555                before = target_block.always[:state.cur_always_task]
556                after = target_block.always[state.cur_always_task:]
557                target_block.always = before + task_list + after
558                state._blocks[state.cur_block] = target_block
559        return state
560
561    def add_tasks(self, host, task_list):
562        self._host_states[host.name] = self._insert_tasks_into_state(self.get_host_state(host), task_list)
563