2014-03-23 16:04:03 +01:00
|
|
|
"""The tasklist module contains the TaskList class.
|
|
|
|
"""
|
|
|
|
|
2014-03-23 23:12:07 +01:00
|
|
|
from bootstrapvz.common.exceptions import TaskListError
|
2013-06-09 20:29:54 +02:00
|
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
2013-05-02 19:13:35 +02:00
|
|
|
|
|
|
|
|
2013-06-23 15:26:08 +02:00
|
|
|
class TaskList(object):
|
2014-03-23 16:04:03 +01:00
|
|
|
"""The tasklist class aggregates all tasks that should be run
|
|
|
|
and orders them according to their dependencies.
|
|
|
|
"""
|
2013-05-16 08:00:28 +02:00
|
|
|
|
2013-06-23 15:26:08 +02:00
|
|
|
def __init__(self):
|
|
|
|
self.tasks = set()
|
2013-06-26 23:40:42 +02:00
|
|
|
self.tasks_completed = []
|
2013-06-23 15:26:08 +02:00
|
|
|
|
2014-01-05 15:13:09 +01:00
|
|
|
def load(self, function, manifest, *args):
|
2014-03-23 16:04:03 +01:00
|
|
|
"""Calls 'function' on the provider and all plugins that have been loaded by the manifest.
|
|
|
|
Any additional arguments are passed directly to 'function'.
|
|
|
|
The function that is called shall accept the taskset as its first argument and the manifest
|
|
|
|
as its second argument.
|
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:param str function: Name of the function to call
|
|
|
|
:param Manifest manifest: The manifest
|
|
|
|
:param list *args: Additional arguments that should be passed to the function that is called
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
|
|
|
# Call 'function' on the provider
|
2014-01-05 15:13:09 +01:00
|
|
|
getattr(manifest.modules['provider'], function)(self.tasks, manifest, *args)
|
|
|
|
for plugin in manifest.modules['plugins']:
|
2014-04-08 21:25:39 +02:00
|
|
|
# Plugins are not required to have whatever function we call
|
2014-01-05 15:13:09 +01:00
|
|
|
fn = getattr(plugin, function, None)
|
|
|
|
if callable(fn):
|
|
|
|
fn(self.tasks, manifest, *args)
|
2013-06-23 15:26:08 +02:00
|
|
|
|
2014-04-07 21:49:34 +02:00
|
|
|
def run(self, info, dry_run=False):
|
2014-03-23 16:04:03 +01:00
|
|
|
"""Converts the taskgraph into a list and runs all tasks in that list
|
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:param dict info: The bootstrap information object
|
|
|
|
:param bool dry_run: Whether to actually run the tasks or simply step through them
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
|
|
|
# Create a list for us to run
|
2014-01-05 15:13:09 +01:00
|
|
|
task_list = self.create_list()
|
2014-03-23 16:04:03 +01:00
|
|
|
# Output the tasklist
|
2014-05-03 22:24:13 +02:00
|
|
|
log.debug('Tasklist:\n\t' + ('\n\t'.join(map(repr, task_list))))
|
2013-06-23 17:03:55 +02:00
|
|
|
|
2014-01-05 15:57:11 +01:00
|
|
|
for task in task_list:
|
2014-03-23 16:04:03 +01:00
|
|
|
# Tasks are not required to have a description
|
2013-06-26 23:40:42 +02:00
|
|
|
if hasattr(task, 'description'):
|
|
|
|
log.info(task.description)
|
|
|
|
else:
|
2014-03-23 16:04:03 +01:00
|
|
|
# If there is no description, simply coerce the task into a string and print its name
|
2014-05-03 22:24:13 +02:00
|
|
|
log.info('Running ' + str(task))
|
2013-10-27 18:37:43 +01:00
|
|
|
if not dry_run:
|
2014-03-23 16:04:03 +01:00
|
|
|
# Run the task
|
2013-10-27 18:37:43 +01:00
|
|
|
task.run(info)
|
2014-03-23 16:04:03 +01:00
|
|
|
# Remember which tasks have been run for later use (e.g. when rolling back, because of an error)
|
2014-01-05 15:57:11 +01:00
|
|
|
self.tasks_completed.append(task)
|
2013-06-24 23:12:39 +02:00
|
|
|
|
2014-01-05 15:13:09 +01:00
|
|
|
def create_list(self):
|
2014-03-23 16:04:03 +01:00
|
|
|
"""Creates a list of all the tasks that should be run.
|
|
|
|
"""
|
2014-03-23 23:12:07 +01:00
|
|
|
from bootstrapvz.common.phases import order
|
2014-02-18 20:45:10 +01:00
|
|
|
# Get a hold of all tasks
|
|
|
|
tasks = self.get_all_tasks()
|
|
|
|
# Make sure the taskset is a subset of all the tasks we have gathered
|
|
|
|
self.tasks.issubset(tasks)
|
|
|
|
# Create a graph over all tasks by creating a map of each tasks successors
|
2013-06-23 17:03:55 +02:00
|
|
|
graph = {}
|
2014-02-18 20:45:10 +01:00
|
|
|
for task in tasks:
|
|
|
|
# Do a sanity check first
|
2013-09-22 17:31:43 +02:00
|
|
|
self.check_ordering(task)
|
|
|
|
successors = set()
|
2014-02-18 20:45:10 +01:00
|
|
|
# Add all successors mentioned in the task
|
2013-11-21 15:54:42 +01:00
|
|
|
successors.update(task.successors)
|
2014-02-18 20:45:10 +01:00
|
|
|
# Add all tasks that mention this task as a predecessor
|
|
|
|
successors.update(filter(lambda succ: task in succ.predecessors, tasks))
|
|
|
|
# Create a list of phases that succeed the phase of this task
|
2013-08-17 17:28:46 +02:00
|
|
|
succeeding_phases = order[order.index(task.phase) + 1:]
|
2014-02-18 20:45:10 +01:00
|
|
|
# Add all tasks that occur in above mentioned succeeding phases
|
|
|
|
successors.update(filter(lambda succ: succ.phase in succeeding_phases, tasks))
|
|
|
|
# Map the successors to the task
|
|
|
|
graph[task] = successors
|
2013-06-23 17:54:25 +02:00
|
|
|
|
2014-03-23 16:04:03 +01:00
|
|
|
# Use the strongly connected components algorithm to check for cycles in our task graph
|
2013-06-23 17:03:55 +02:00
|
|
|
components = self.strongly_connected_components(graph)
|
|
|
|
cycles_found = 0
|
|
|
|
for component in components:
|
2014-03-23 16:04:03 +01:00
|
|
|
# Node of 1 is also a strongly connected component but hardly a cycle, so we filter them out
|
2013-06-23 17:03:55 +02:00
|
|
|
if len(component) > 1:
|
|
|
|
cycles_found += 1
|
2014-05-03 22:24:13 +02:00
|
|
|
log.debug('Cycle: {list}\n' + (', '.join(map(repr, component))))
|
2013-06-23 17:03:55 +02:00
|
|
|
if cycles_found > 0:
|
2014-05-03 22:24:13 +02:00
|
|
|
msg = ('{num} cycles were found in the tasklist, '
|
|
|
|
'consult the logfile for more information.'.format(num=cycles_found))
|
2013-06-23 17:03:55 +02:00
|
|
|
raise TaskListError(msg)
|
|
|
|
|
2014-02-18 20:45:10 +01:00
|
|
|
# Run a topological sort on the graph, returning an ordered list
|
2013-06-23 17:03:55 +02:00
|
|
|
sorted_tasks = self.topological_sort(graph)
|
|
|
|
|
2014-02-18 20:45:10 +01:00
|
|
|
# Filter out any tasks not in the tasklist
|
|
|
|
# We want to maintain ordering, so we don't use set intersection
|
|
|
|
sorted_tasks = filter(lambda task: task in self.tasks, sorted_tasks)
|
2013-06-23 17:54:25 +02:00
|
|
|
return sorted_tasks
|
2013-06-23 17:03:55 +02:00
|
|
|
|
2014-02-18 20:45:10 +01:00
|
|
|
def get_all_tasks(self):
|
2014-03-23 16:04:03 +01:00
|
|
|
"""Gets a list of all task classes in the package
|
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:return: A list of all tasks in the package
|
|
|
|
:rtype: list
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
2014-02-18 20:45:10 +01:00
|
|
|
# Get a generator that returns all classes in the package
|
2014-03-23 23:12:07 +01:00
|
|
|
import os.path
|
|
|
|
pkg_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
classes = self.get_all_classes(pkg_path, 'bootstrapvz.')
|
2014-02-18 20:45:10 +01:00
|
|
|
|
|
|
|
# lambda function to check whether a class is a task (excluding the superclass Task)
|
|
|
|
def is_task(obj):
|
|
|
|
from task import Task
|
|
|
|
return issubclass(obj, Task) and obj is not Task
|
|
|
|
return filter(is_task, classes) # Only return classes that are tasks
|
|
|
|
|
2014-03-23 23:12:07 +01:00
|
|
|
def get_all_classes(self, path=None, prefix=''):
|
2014-03-23 16:04:03 +01:00
|
|
|
""" Given a path to a package, this function retrieves all the classes in it
|
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:param str path: Path to the package
|
|
|
|
:param str prefix: Name of the package followed by a dot
|
|
|
|
:return: A generator that yields classes
|
|
|
|
:rtype: generator
|
|
|
|
:raises Exception: If a module cannot be inspected.
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
2014-02-18 20:45:10 +01:00
|
|
|
import pkgutil
|
|
|
|
import importlib
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
def walk_error(module):
|
2014-05-03 22:24:13 +02:00
|
|
|
raise Exception('Unable to inspect module ' + module)
|
2014-03-23 23:12:07 +01:00
|
|
|
walker = pkgutil.walk_packages([path], prefix, walk_error)
|
2014-02-18 20:45:10 +01:00
|
|
|
for _, module_name, _ in walker:
|
|
|
|
module = importlib.import_module(module_name)
|
|
|
|
classes = inspect.getmembers(module, inspect.isclass)
|
|
|
|
for class_name, obj in classes:
|
|
|
|
# We only want classes that are defined in the module, and not imported ones
|
|
|
|
if obj.__module__ == module_name:
|
|
|
|
yield obj
|
|
|
|
|
2013-09-22 17:31:43 +02:00
|
|
|
def check_ordering(self, task):
|
2014-05-04 19:31:53 +02:00
|
|
|
"""Checks the ordering of a task in relation to other tasks and their phases.
|
|
|
|
|
2014-03-23 16:04:03 +01:00
|
|
|
This function checks for a subset of what the strongly connected components algorithm does,
|
|
|
|
but can deliver a more precise error message, namely that there is a conflict between
|
|
|
|
what a task has specified as its predecessors or successors and in which phase it is placed.
|
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:param Task task: The task to check the ordering for
|
|
|
|
:raises TaskListError: If there is a conflict between task precedence and phase precedence
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
2013-11-21 15:54:42 +01:00
|
|
|
for successor in task.successors:
|
2014-03-23 16:04:03 +01:00
|
|
|
# Run through all successors and check whether the phase of the task
|
|
|
|
# comes before the phase of a successor
|
2014-04-07 21:50:06 +02:00
|
|
|
if task.phase > successor.phase:
|
2013-09-22 17:31:43 +02:00
|
|
|
msg = ("The task {task} is specified as running before {other}, "
|
|
|
|
"but its phase '{phase}' lies after the phase '{other_phase}'"
|
|
|
|
.format(task=task, other=successor, phase=task.phase, other_phase=successor.phase))
|
|
|
|
raise TaskListError(msg)
|
2013-11-21 15:54:42 +01:00
|
|
|
for predecessor in task.predecessors:
|
2014-03-23 16:04:03 +01:00
|
|
|
# Run through all predecessors and check whether the phase of the task
|
|
|
|
# comes after the phase of a predecessor
|
2013-09-22 17:31:43 +02:00
|
|
|
if task.phase < predecessor.phase:
|
|
|
|
msg = ("The task {task} is specified as running after {other}, "
|
|
|
|
"but its phase '{phase}' lies before the phase '{other_phase}'"
|
|
|
|
.format(task=task, other=predecessor, phase=task.phase, other_phase=predecessor.phase))
|
|
|
|
raise TaskListError(msg)
|
|
|
|
|
2013-06-23 17:03:55 +02:00
|
|
|
def strongly_connected_components(self, graph):
|
2014-03-23 16:04:03 +01:00
|
|
|
"""Find the strongly connected components in a graph using Tarjan's algorithm.
|
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
Source: http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py
|
2014-03-23 16:04:03 +01:00
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:param dict graph: mapping of tasks to lists of successor tasks
|
|
|
|
:return: List of tuples that are strongly connected comoponents
|
|
|
|
:rtype: list
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
2013-06-23 17:03:55 +02:00
|
|
|
|
2013-06-26 20:14:37 +02:00
|
|
|
result = []
|
|
|
|
stack = []
|
|
|
|
low = {}
|
2013-06-23 17:03:55 +02:00
|
|
|
|
|
|
|
def visit(node):
|
2013-06-26 20:14:37 +02:00
|
|
|
if node in low:
|
|
|
|
return
|
2013-06-23 17:03:55 +02:00
|
|
|
|
|
|
|
num = len(low)
|
|
|
|
low[node] = num
|
|
|
|
stack_pos = len(stack)
|
|
|
|
stack.append(node)
|
|
|
|
|
|
|
|
for successor in graph[node]:
|
|
|
|
visit(successor)
|
|
|
|
low[node] = min(low[node], low[successor])
|
|
|
|
|
|
|
|
if num == low[node]:
|
|
|
|
component = tuple(stack[stack_pos:])
|
|
|
|
del stack[stack_pos:]
|
|
|
|
result.append(component)
|
|
|
|
for item in component:
|
|
|
|
low[item] = len(graph)
|
|
|
|
|
|
|
|
for node in graph:
|
|
|
|
visit(node)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
def topological_sort(self, graph):
|
2014-05-04 19:31:53 +02:00
|
|
|
"""Runs a topological sort on a graph.
|
2014-03-23 16:04:03 +01:00
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
Source: http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py
|
2014-03-23 16:04:03 +01:00
|
|
|
|
2014-05-04 19:31:53 +02:00
|
|
|
:param dict graph: mapping of tasks to lists of successor tasks
|
|
|
|
:return: A list of all tasks in the graph sorted according to ther dependencies
|
|
|
|
:rtype: list
|
2014-03-23 16:04:03 +01:00
|
|
|
"""
|
2013-06-26 20:14:37 +02:00
|
|
|
count = {}
|
2013-06-23 17:03:55 +02:00
|
|
|
for node in graph:
|
|
|
|
count[node] = 0
|
|
|
|
for node in graph:
|
|
|
|
for successor in graph[node]:
|
|
|
|
count[successor] += 1
|
|
|
|
|
2013-06-26 20:14:37 +02:00
|
|
|
ready = [node for node in graph if count[node] == 0]
|
2013-06-23 17:03:55 +02:00
|
|
|
|
2013-06-26 20:14:37 +02:00
|
|
|
result = []
|
2013-06-23 17:03:55 +02:00
|
|
|
while ready:
|
|
|
|
node = ready.pop(-1)
|
|
|
|
result.append(node)
|
|
|
|
|
|
|
|
for successor in graph[node]:
|
|
|
|
count[successor] -= 1
|
|
|
|
if count[successor] == 0:
|
|
|
|
ready.append(successor)
|
|
|
|
|
|
|
|
return result
|