import os import logging import shutil import random from copy import copy from collections import OrderedDict, defaultdict from wa.framework import pluginloader, signal, log from wa.framework.run import Runner, RunnerJob from wa.framework.output import RunOutput from wa.framework.actor import JobActor from wa.framework.resource import ResourceResolver from wa.framework.exception import ConfigError, NotFoundError from wa.framework.configuration import ConfigurationPoint, PluginConfiguration, WA_CONFIGURATION from wa.utils.serializer import read_pod from wa.utils.misc import ensure_directory_exists as _d, Namespace from wa.utils.types import list_of, identifier, caseless_string __all__ = [ 'Executor', 'ExecutionOutput', 'ExecutionwContext', 'ExecuteWorkloadContainerActor', 'ExecuteWorkloadJobActor', ] class Executor(object): def __init__(self, output): self.output = output self.config = ExecutionRunConfiguration() self.agenda_string = None self.agenda = None self.jobs = None self.container = None self.target = None def load_config(self, filepath): self.config.update(filepath) def load_agenda(self, agenda_string): if self.agenda: raise RuntimeError('Only one agenda may be loaded per run.') self.agenda_string = agenda_string if os.path.isfile(agenda_string): self.logger.debug('Loading agenda from {}'.format(agenda_string)) self.agenda = Agenda(agenda_string) shutil.copy(agenda_string, self.output.config_directory) else: self.logger.debug('"{}" is not a file; assuming workload name.'.format(agenda_string)) self.agenda = Agenda() self.agenda.add_workload_entry(agenda_string) def disable_instrument(self, name): if not self.agenda: raise RuntimeError('initialize() must be invoked before disable_instrument()') self.agenda.config['instrumentation'].append('~{}'.format(itd)) def initialize(self): if not self.agenda: raise RuntimeError('No agenda has been loaded.') self.config.update(self.agenda.config) self.config.consolidate() self._initialize_target() self._initialize_job_config() def execute(self, selectors=None): pass def finalize(self): pass def _initialize_target(self): pass def _initialize_job_config(self): self.agenda.expand(self.target) for tup in agenda_iterator(self.agenda, self.config.execution_order): glob, sect, workload, iter_number = tup def agenda_iterator(agenda, order): """ Iterates over all job components in an agenda, yielding tuples in the form :: (global_entry, section_entry, workload_entry, iteration_number) Which fully define the job to be crated. The order in which these tuples are yielded is determined by the ``order`` parameter which may be one of the following values: ``"by_iteration"`` The first iteration of each workload spec is executed one after the other, so all workloads are executed before proceeding on to the second iteration. E.g. A1 B1 C1 A2 C2 A3. This is the default if no order is explicitly specified. In case of multiple sections, this will spread them out, such that specs from the same section are further part. E.g. given sections X and Y, global specs A and B, and two iterations, this will run :: X.A1, Y.A1, X.B1, Y.B1, X.A2, Y.A2, X.B2, Y.B2 ``"by_section"`` Same as ``"by_iteration"``, however this will group specs from the same section together, so given sections X and Y, global specs A and B, and two iterations, this will run :: X.A1, X.B1, Y.A1, Y.B1, X.A2, X.B2, Y.A2, Y.B2 ``"by_spec"`` All iterations of the first spec are executed before moving on to the next spec. E.g. A1 A2 A3 B1 C1 C2. ``"random"`` Execution order is entirely random. """ # TODO: this would be a place to perform section expansions. # (e.g. sweeps, cross-products, etc). global_iterations = agenda.global_.number_of_iterations all_iterations = [global_iterations] all_iterations.extend([s.number_of_iterations for s in agenda.sections]) all_iterations.extend([w.number_of_iterations for w in agenda.workloads]) max_iterations = max(all_iterations) if order == 'by_spec': if agenda.sections: for section in agenda.sections: section_iterations = section.number_of_iterations or global_iterations for workload in agenda.workloads + section.workloads: workload_iterations = workload.number_of_iterations or section_iterations for i in xrange(workload_iterations): yield agenda.global_, section, workload, i else: # not sections for workload in agenda.workloads: workload_iterations = workload.number_of_iterations or global_iterations for i in xrange(workload_iterations): yield agenda.global_, None, workload, i elif order == 'by_section': for i in xrange(max_iterations): if agenda.sections: for section in agenda.sections: section_iterations = section.number_of_iterations or global_iterations for workload in agenda.workloads + section.workloads: workload_iterations = workload.number_of_iterations or section_iterations if i < workload_iterations: yield agenda.global_, section, workload, i else: # not sections for workload in agenda.workloads: workload_iterations = workload.number_of_iterations or global_iterations if i < workload_iterations: yield agenda.global_, None, workload, i elif order == 'by_iteration': for i in xrange(max_iterations): if agenda.sections: for workload in agenda.workloads: for section in agenda.sections: section_iterations = section.number_of_iterations or global_iterations workload_iterations = workload.number_of_iterations or section_iterations or global_iterations if i < workload_iterations: yield agenda.global_, section, workload, i # Now do the section-specific workloads for section in agenda.sections: section_iterations = section.number_of_iterations or global_iterations for workload in section.workloads: workload_iterations = workload.number_of_iterations or section_iterations or global_iterations if i < workload_iterations: yield agenda.global_, section, workload, i else: # not sections for workload in agenda.workloads: workload_iterations = workload.number_of_iterations or global_iterations if i < workload_iterations: yield agenda.global_, None, workload, i elif order == 'random': tuples = list(agenda_iterator(data, order='by_section')) random.shuffle(tuples) for t in tuples: yield t else: raise ValueError('Invalid order: "{}"'.format(order)) class RebootPolicy(object): """ Represents the reboot policy for the execution -- at what points the device should be rebooted. This, in turn, is controlled by the policy value that is passed in on construction and would typically be read from the user's settings. Valid policy values are: :never: The device will never be rebooted. :as_needed: Only reboot the device if it becomes unresponsive, or needs to be flashed, etc. :initial: The device will be rebooted when the execution first starts, just before executing the first workload spec. :each_spec: The device will be rebooted before running a new workload spec. :each_iteration: The device will be rebooted before each new iteration. """ valid_policies = ['never', 'as_needed', 'initial', 'each_spec', 'each_iteration'] def __init__(self, policy): policy = policy.strip().lower().replace(' ', '_') if policy not in self.valid_policies: message = 'Invalid reboot policy {}; must be one of {}'.format(policy, ', '.join(self.valid_policies)) raise ConfigError(message) self.policy = policy @property def can_reboot(self): return self.policy != 'never' @property def perform_initial_boot(self): return self.policy not in ['never', 'as_needed'] @property def reboot_on_each_spec(self): return self.policy in ['each_spec', 'each_iteration'] @property def reboot_on_each_iteration(self): return self.policy == 'each_iteration' def __str__(self): return self.policy __repr__ = __str__ def __cmp__(self, other): if isinstance(other, RebootPolicy): return cmp(self.policy, other.policy) else: return cmp(self.policy, other) class RuntimeParameterSetter(object): """ Manages runtime parameter state during execution. """ @property def target(self): return self.target_assistant.target def __init__(self, target_assistant): self.target_assistant = target_assistant self.to_set = defaultdict(list) # name --> list of values self.last_set = {} self.to_unset = defaultdict(int) # name --> count def validate(self, params): self.target_assistant.validate_runtime_parameters(params) def mark_set(self, params): for name, value in params.iteritems(): self.to_set[name].append(value) def mark_unset(self, params): for name in params.iterkeys(): self.to_unset[name] += 1 def inact_set(self): self.target_assistant.clear_parameters() for name in self.to_set: self._set_if_necessary(name) self.target_assitant.set_parameters() def inact_unset(self): self.target_assistant.clear_parameters() for name, count in self.to_unset.iteritems(): while count: self.to_set[name].pop() count -= 1 self._set_if_necessary(name) self.target_assitant.set_parameters() def _set_if_necessary(self, name): if not self.to_set[name]: return new_value = self.to_set[name][-1] prev_value = self.last_set.get(name) if new_value != prev_value: self.target_assistant.add_paramter(name, new_value) self.last_set[name] = new_value class WorkloadExecutionConfig(object): @staticmethod def from_pod(pod): return WorkloadExecutionConfig(**pod) def __init__(self, workload_name, workload_parameters=None, runtime_parameters=None, components=None, assumptions=None): self.workload_name = workload_name or None self.workload_parameters = workload_parameters or {} self.runtime_parameters = runtime_parameters or {} self.components = components or {} self.assumpations = assumptions or {} def to_pod(self): return copy(self.__dict__) class WorkloadExecutionActor(JobActor): def __init__(self, target, config, loader=pluginloader): self.target = target self.config = config self.logger = logging.getLogger('exec') self.context = None self.workload = loader.get_workload(config.workload_name, target, **config.workload_parameters) def get_config(self): return self.config.to_pod() def initialize(self, context): self.context = context self.workload.init_resources(self.context) self.workload.validate() self.workload.initialize(self.context) def run(self): if not self.workload: self.logger.warning('Failed to initialize workload; skipping execution') return self.pre_run() self.logger.info('Setting up workload') with signal.wrap('WORKLOAD_SETUP'): self.workload.setup(self.context) try: error = None self.logger.info('Executing workload') try: with signal.wrap('WORKLOAD_EXECUTION'): self.workload.run(self.context) except Exception as e: log.log_error(e, self.logger) error = e self.logger.info('Processing execution results') with signal.wrap('WORKLOAD_RESULT_UPDATE'): if not error: self.workload.update_result(self.context) else: self.logger.info('Workload execution failed; not extracting workload results.') raise error finally: if self.target.check_responsive(): self.logger.info('Tearing down workload') with signal.wrap('WORKLOAD_TEARDOWN'): self.workload.teardown(self.context) self.post_run() def finalize(self): self.workload.finalize(self.context) def pre_run(self): # TODO: enable components, etc pass def post_run(self): pass