Skip to content

Instantly share code, notes, and snippets.

@azertyfun
Created March 11, 2020 15:07
Show Gist options
  • Save azertyfun/70b5341310840f92636ecf3ea2c3c6d3 to your computer and use it in GitHub Desktop.
Save azertyfun/70b5341310840f92636ecf3ea2c3c6d3 to your computer and use it in GitHub Desktop.
Ansible linear strategy that supports the START_AT_TASK environment variable
#!/usr/bin/env python3
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
strategy: linear_checkpoints
short_description: Executes tasks in a linear fashion, allow start-at-task to run `always` tags
description:
- Task execution is in lockstep per host batch as defined by C(serial) (default all).
Up to the fork limit of hosts will execute each task at the same time and then
the next series of hosts until the batch is done, before going on to the next task.
When using the START_AT_TASK environment variable, the playbook behaves as if
--start-at-task was used, except the tasks with the `always` tag will also be run,
rather than skipped using the regular `linear` strategy.
version_added: "2.8"
notes:
- This was the default Ansible behaviour before 'strategy plugins' were introduced in 2.0.
author: Nathan Monfils (Destiny NV)
'''
import fnmatch
import os
from ansible.errors import AnsibleError, AnsibleAssertionError
from ansible.executor.play_iterator import PlayIterator
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_text
from ansible.playbook.block import Block
from ansible.playbook.included_file import IncludedFile
from ansible.playbook.task import Task
from ansible.plugins.loader import action_loader
from ansible.plugins.strategy import StrategyBase
from ansible.template import Templar
from ansible.utils.display import Display
from ansible.plugins.strategy.linear import StrategyModule as LinearStrategyModule
display = Display()
class StrategyModule(LinearStrategyModule):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_at_task = os.getenv('START_AT_TASK')
self.started_at_task = False
# This function has only been very lightly modified
# Look for: "This is the bit that has been modified"
# - Nathan Monfils
def _get_next_task_lockstep(self, hosts, iterator):
'''
Returns a list of (host, task) tuples, where the task may
be a noop task to keep the iterator in lock step across
all hosts.
'''
noop_task = Task()
noop_task.action = 'meta'
noop_task.args['_raw_params'] = 'noop'
noop_task.set_loader(iterator._play._loader)
host_tasks = {}
display.debug("building list of next tasks for hosts")
for host in hosts:
host_tasks[host.name] = iterator.get_next_task_for_host(host, peek=True)
display.debug("done building task lists")
num_setups = 0
num_tasks = 0
num_rescue = 0
num_always = 0
display.debug("counting tasks in each state of execution")
host_tasks_to_run = [(host, state_task)
for host, state_task in iteritems(host_tasks)
if state_task and state_task[1]]
if host_tasks_to_run:
try:
lowest_cur_block = min(
(iterator.get_active_state(s).cur_block for h, (s, t) in host_tasks_to_run
if s.run_state != PlayIterator.ITERATING_COMPLETE))
except ValueError:
lowest_cur_block = None
else:
# empty host_tasks_to_run will just run till the end of the function
# without ever touching lowest_cur_block
lowest_cur_block = None
for (k, v) in host_tasks_to_run:
(s, t) = v
s = iterator.get_active_state(s)
if s.cur_block > lowest_cur_block:
# Not the current block, ignore it
continue
if s.run_state == PlayIterator.ITERATING_SETUP:
num_setups += 1
elif s.run_state == PlayIterator.ITERATING_TASKS:
num_tasks += 1
elif s.run_state == PlayIterator.ITERATING_RESCUE:
num_rescue += 1
elif s.run_state == PlayIterator.ITERATING_ALWAYS:
num_always += 1
display.debug("done counting tasks in each state of execution:\n\tnum_setups: %s\n\tnum_tasks: %s\n\tnum_rescue: %s\n\tnum_always: %s" % (num_setups,
num_tasks,
num_rescue,
num_always))
def _advance_selected_hosts(hosts, cur_block, cur_state):
'''
This helper returns the task for all hosts in the requested
state, otherwise they get a noop dummy task. This also advances
the state of the host, since the given states are determined
while using peek=True.
'''
# we return the values in the order they were originally
# specified in the given hosts array
rvals = []
display.debug("starting to advance hosts")
for host in hosts:
host_state_task = host_tasks.get(host.name)
if host_state_task is None:
continue
(s, t) = host_state_task
s = iterator.get_active_state(s)
if t is None:
continue
# This is the bit that has been modified
skipping = False
if self.start_at_task and not self.started_at_task:
if t.name == self.start_at_task or fnmatch.fnmatch(t.name, self.start_at_task) or t.get_name() == self.start_at_task or fnmatch.fnmatch(t.get_name(), self.start_at_task):
self.started_at_task = True
else:
display.debug("skipping task %s (%s) != %s (start-at-task)" % (t.name, t.get_name(), self.start_at_task))
skipping = 'always' not in t.tags
if s.run_state == cur_state and s.cur_block == cur_block:
new_t = iterator.get_next_task_for_host(host)
if not skipping:
rvals.append((host, t))
else:
rvals.append((host, noop_task))
else:
rvals.append((host, noop_task))
display.debug("done advancing hosts to next task")
return rvals
# if any hosts are in ITERATING_SETUP, return the setup task
# while all other hosts get a noop
if num_setups:
display.debug("advancing hosts in ITERATING_SETUP")
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_SETUP)
# if any hosts are in ITERATING_TASKS, return the next normal
# task for these hosts, while all other hosts get a noop
if num_tasks:
display.debug("advancing hosts in ITERATING_TASKS")
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_TASKS)
# if any hosts are in ITERATING_RESCUE, return the next rescue
# task for these hosts, while all other hosts get a noop
if num_rescue:
display.debug("advancing hosts in ITERATING_RESCUE")
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_RESCUE)
# if any hosts are in ITERATING_ALWAYS, return the next always
# task for these hosts, while all other hosts get a noop
if num_always:
display.debug("advancing hosts in ITERATING_ALWAYS")
return _advance_selected_hosts(hosts, lowest_cur_block, PlayIterator.ITERATING_ALWAYS)
# at this point, everything must be ITERATING_COMPLETE, so we
# return None for all hosts in the list
display.debug("all hosts are done, so returning None's for all hosts")
return [(host, None) for host in hosts]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment