| #pylint: disable=C0111 |
| |
| """ |
| Prejob tasks. |
| |
| Prejob tasks _usually_ run before a job and verify the state of a machine. |
| Cleanup and repair are exceptions, cleanup can run after a job too, while |
| repair will run anytime the host needs a repair, which could be pre or post |
| job. Most of the work specific to this module is achieved through the prolog |
| and epilog of each task. |
| |
| All prejob tasks must have a host, though they may not have an HQE. If a |
| prejob task has a hqe, it will activate the hqe through its on_pending |
| method on successful completion. A row in afe_special_tasks with values: |
| host=C1, unlocked, is_active=0, is_complete=0, type=Verify |
| will indicate to the scheduler that it needs to schedule a new special task |
| of type=Verify, against the C1 host. While the special task is running |
| the scheduler only monitors it through the Agent, and its is_active bit=1. |
| Once a special task finishes, we set its is_active=0, is_complete=1 and |
| success bits, so the scheduler ignores it. |
| HQE.on_pending: |
| Host, HQE -> Pending, Starting |
| This status is acted upon in the scheduler, to assign an AgentTask. |
| PreJobTask: |
| epilog: |
| failure: |
| requeue hqe |
| repair the host |
| Children PreJobTasks: |
| prolog: |
| set Host, HQE status |
| epilog: |
| success: |
| on_pending |
| failure: |
| repair throgh PreJobTask |
| set Host, HQE status |
| |
| Failing a prejob task effects both the Host and the HQE, as follows: |
| |
| - Host: PreJob failure will result in a Repair job getting queued against |
| the host, is we haven't already tried repairing it more than the |
| max_repair_limit. When this happens, the host will remain in whatever status |
| the prejob task left it in, till the Repair job puts it into 'Repairing'. This |
| way the host_scheduler won't pick bad hosts and assign them to jobs. |
| |
| If we have already tried repairing the host too many times, the PreJobTask |
| will flip the host to 'RepairFailed' in its epilog, and it will remain in this |
| state till it is recovered and reverified. |
| |
| - HQE: Is either requeued or failed. Requeuing the HQE involves putting it |
| in the Queued state and setting its host_id to None, so it gets a new host |
| in the next scheduler tick. Failing the HQE results in either a Parsing |
| or Archiving postjob task, and an eventual Failed status for the HQE. |
| """ |
| |
| import logging |
| import re |
| |
| from autotest_lib.client.common_lib import host_protections |
| from autotest_lib.frontend.afe import models |
| from autotest_lib.scheduler import agent_task, scheduler_config |
| from autotest_lib.server import autoserv_utils |
| from autotest_lib.server.cros import provision |
| |
| |
| class PreJobTask(agent_task.SpecialAgentTask): |
| def epilog(self): |
| super(PreJobTask, self).epilog() |
| |
| if self.host.protection == host_protections.Protection.DO_NOT_VERIFY: |
| # effectively ignore failure for these hosts |
| self.success = True |
| |
| if self.success: |
| self.host.record_working_state(True, |
| self.task.time_finished) |
| return |
| |
| if self.queue_entry: |
| # If we requeue a HQE, we should cancel any remaining pre-job |
| # tasks against this host, otherwise we'll be left in a state |
| # where a queued HQE has special tasks to run against a host. |
| models.SpecialTask.objects.filter( |
| queue_entry__id=self.queue_entry.id, |
| host__id=self.host.id, |
| is_complete=0).update(is_complete=1, success=0) |
| |
| previous_provisions = models.SpecialTask.objects.filter( |
| task=models.SpecialTask.Task.PROVISION, |
| queue_entry_id=self.queue_entry.id).count() |
| if (previous_provisions > |
| scheduler_config.config.max_provision_retries): |
| self._actually_fail_queue_entry() |
| # This abort will mark the aborted bit on the HQE itself, to |
| # signify that we're killing it. Technically it also will do |
| # the recursive aborting of all child jobs, but that shouldn't |
| # matter here, as only suites have children, and those are |
| # hostless and thus don't have provisioning. |
| # TODO(milleral) http://crbug.com/188217 |
| # However, we can't actually do this yet, as if we set the |
| # abort bit the FinalReparseTask will set the status of the HQE |
| # to ABORTED, which then means that we don't show the status in |
| # run_suite. So in the meantime, don't mark the HQE as |
| # aborted. |
| # queue_entry.abort() |
| else: |
| # requeue() must come after handling provision retries, since |
| # _actually_fail_queue_entry needs an execution subdir. |
| # We also don't want to requeue if we hit the provision retry |
| # limit, since then we overwrite the PARSING state of the HQE. |
| self.queue_entry.requeue() |
| |
| # Limit the repair on a host when a prejob task fails, e.g., reset, |
| # verify etc. The number of repair jobs is limited to the specific |
| # HQE and host. |
| previous_repairs = models.SpecialTask.objects.filter( |
| task=models.SpecialTask.Task.REPAIR, |
| queue_entry_id=self.queue_entry.id, |
| host_id=self.queue_entry.host_id).count() |
| if previous_repairs >= scheduler_config.config.max_repair_limit: |
| self.host.set_status(models.Host.Status.REPAIR_FAILED) |
| self._fail_queue_entry() |
| return |
| |
| queue_entry = models.HostQueueEntry.objects.get( |
| id=self.queue_entry.id) |
| else: |
| queue_entry = None |
| |
| models.SpecialTask.objects.create( |
| host=models.Host.objects.get(id=self.host.id), |
| task=models.SpecialTask.Task.REPAIR, |
| queue_entry=queue_entry, |
| requested_by=self.task.requested_by) |
| |
| |
| def _should_pending(self): |
| """ |
| Decide if we should call the host queue entry's on_pending method. |
| We should if: |
| 1) There exists an associated host queue entry. |
| 2) The current special task completed successfully. |
| 3) There do not exist any more special tasks to be run before the |
| host queue entry starts. |
| |
| @returns: True if we should call pending, false if not. |
| |
| """ |
| if not self.queue_entry or not self.success: |
| return False |
| |
| # We know if this is the last one when we create it, so we could add |
| # another column to the database to keep track of this information, but |
| # I expect the overhead of querying here to be minimal. |
| queue_entry = models.HostQueueEntry.objects.get(id=self.queue_entry.id) |
| queued = models.SpecialTask.objects.filter( |
| host__id=self.host.id, is_active=False, |
| is_complete=False, queue_entry=queue_entry) |
| queued = queued.exclude(id=self.task.id) |
| return queued.count() == 0 |
| |
| |
| class VerifyTask(PreJobTask): |
| TASK_TYPE = models.SpecialTask.Task.VERIFY |
| |
| |
| def __init__(self, task): |
| args = ['-v'] |
| if task.queue_entry: |
| args.extend(self._generate_autoserv_label_args(task)) |
| super(VerifyTask, self).__init__(task, args) |
| self._set_ids(host=self.host, queue_entries=[self.queue_entry]) |
| |
| |
| def prolog(self): |
| super(VerifyTask, self).prolog() |
| |
| logging.info("starting verify on %s", self.host.hostname) |
| if self.queue_entry: |
| self.queue_entry.set_status(models.HostQueueEntry.Status.VERIFYING) |
| self.host.set_status(models.Host.Status.VERIFYING) |
| |
| # Delete any queued manual reverifies for this host. One verify will do |
| # and there's no need to keep records of other requests. |
| self.remove_special_tasks(models.SpecialTask.Task.VERIFY, |
| keep_last_one=True) |
| |
| |
| def epilog(self): |
| super(VerifyTask, self).epilog() |
| if self.success: |
| if self._should_pending(): |
| self.queue_entry.on_pending() |
| else: |
| self.host.set_status(models.Host.Status.READY) |
| |
| |
| class CleanupTask(PreJobTask): |
| # note this can also run post-job, but when it does, it's running standalone |
| # against the host (not related to the job), so it's not considered a |
| # PostJobTask |
| |
| TASK_TYPE = models.SpecialTask.Task.CLEANUP |
| |
| |
| def __init__(self, task, recover_run_monitor=None): |
| args = ['--cleanup'] |
| if task.queue_entry: |
| args.extend(self._generate_autoserv_label_args(task)) |
| super(CleanupTask, self).__init__(task, args) |
| self._set_ids(host=self.host, queue_entries=[self.queue_entry]) |
| |
| |
| def prolog(self): |
| super(CleanupTask, self).prolog() |
| logging.info("starting cleanup task for host: %s", self.host.hostname) |
| self.host.set_status(models.Host.Status.CLEANING) |
| if self.queue_entry: |
| self.queue_entry.set_status(models.HostQueueEntry.Status.CLEANING) |
| |
| |
| def _finish_epilog(self): |
| if not self.queue_entry or not self.success: |
| return |
| |
| do_not_verify_protection = host_protections.Protection.DO_NOT_VERIFY |
| should_run_verify = ( |
| self.queue_entry.job.run_verify |
| and self.host.protection != do_not_verify_protection) |
| if should_run_verify: |
| entry = models.HostQueueEntry.objects.get(id=self.queue_entry.id) |
| models.SpecialTask.objects.create( |
| host=models.Host.objects.get(id=self.host.id), |
| queue_entry=entry, |
| task=models.SpecialTask.Task.VERIFY) |
| else: |
| if self._should_pending(): |
| self.queue_entry.on_pending() |
| |
| |
| def epilog(self): |
| super(CleanupTask, self).epilog() |
| |
| if self.success: |
| self.host.update_field('dirty', 0) |
| self.host.set_status(models.Host.Status.READY) |
| |
| self._finish_epilog() |
| |
| |
| class ResetTask(PreJobTask): |
| """Task to reset a DUT, including cleanup and verify.""" |
| # note this can also run post-job, but when it does, it's running standalone |
| # against the host (not related to the job), so it's not considered a |
| # PostJobTask |
| |
| TASK_TYPE = models.SpecialTask.Task.RESET |
| |
| |
| def __init__(self, task, recover_run_monitor=None): |
| args = ['--reset'] |
| if task.queue_entry: |
| args.extend(self._generate_autoserv_label_args(task)) |
| super(ResetTask, self).__init__(task, args) |
| self._set_ids(host=self.host, queue_entries=[self.queue_entry]) |
| |
| |
| def prolog(self): |
| super(ResetTask, self).prolog() |
| logging.info('starting reset task for host: %s', |
| self.host.hostname) |
| self.host.set_status(models.Host.Status.RESETTING) |
| if self.queue_entry: |
| self.queue_entry.set_status(models.HostQueueEntry.Status.RESETTING) |
| |
| # Delete any queued cleanups for this host. |
| self.remove_special_tasks(models.SpecialTask.Task.CLEANUP, |
| keep_last_one=False) |
| |
| # Delete any queued reverifies for this host. |
| self.remove_special_tasks(models.SpecialTask.Task.VERIFY, |
| keep_last_one=False) |
| |
| # Only one reset is needed. |
| self.remove_special_tasks(models.SpecialTask.Task.RESET, |
| keep_last_one=True) |
| |
| |
| def epilog(self): |
| super(ResetTask, self).epilog() |
| |
| if self.success: |
| self.host.update_field('dirty', 0) |
| |
| if self._should_pending(): |
| self.queue_entry.on_pending() |
| else: |
| self.host.set_status(models.Host.Status.READY) |
| |
| |
| # TODO (ayatane): Refactor using server/cros/provision |
| def _is_cros_version(label): |
| """Return whether the label is a cros-version: label.""" |
| return label.startswith('cros-version:') |
| |
| |
| # TODO (ayatane): Refactor using server/cros/provision |
| def _get_cros_version(label): |
| """Return cros-version from cros-version label.""" |
| return label[len('cros-version:'):] |
| |
| |
| # TODO (ayatane): Refactor into server/cros/provision |
| class _CrosImage(object): |
| """The name of a CrOS image.""" |
| |
| _name_pattern = re.compile( |
| r'^' |
| r'(?P<group>[a-z0-9-]+)' |
| r'/' |
| r'(?P<milestone>LATEST|R[0-9]+)' |
| r'-' |
| r'(?P<version>[0-9.]+)' |
| r'(-(?P<rc>rc[0-9]+))?' |
| r'$' |
| ) |
| |
| def __init__(self, name): |
| """Initialize instance. |
| |
| @param name: Image name string (lumpy-release/R27-3773.0.0) |
| """ |
| self._name = name |
| match = self._name_pattern.search(name) |
| if match is None: |
| raise ValueError('Invalid CrOS image name: %r' % name) |
| self.group = match.group('group') |
| self.milestone = match.group('milestone') |
| self.version = match.group('version') |
| self.rc = match.group('rc') |
| |
| def __repr__(self): |
| return '{cls}({name!r})'.format(cls=type(self).__name__, |
| name=self._name) |
| |
| def __str__(self): |
| return self._name |
| |
| |
| class ProvisionTask(PreJobTask): |
| TASK_TYPE = models.SpecialTask.Task.PROVISION |
| |
| def __init__(self, task): |
| # Provisioning requires that we be associated with a job/queue entry |
| assert task.queue_entry, "No HQE associated with provision task!" |
| # task.queue_entry is an afe model HostQueueEntry object. |
| # self.queue_entry is a scheduler models HostQueueEntry object, but |
| # it gets constructed and assigned in __init__, so it's not available |
| # yet. Therefore, we're stuck pulling labels off of the afe model |
| # so that we can pass the --provision args into the __init__ call. |
| labels = {x.name for x in task.queue_entry.job.labels} |
| _, provisionable = provision.Provision.partition(labels) |
| extra_command_args = ['--provision', |
| '--job-labels', ','.join(provisionable)] |
| super(ProvisionTask, self).__init__(task, extra_command_args) |
| self._set_milestone(labels) |
| self._set_ids(host=self.host, queue_entries=[self.queue_entry]) |
| |
| |
| def _set_milestone(self, labels): |
| """Set build milestone from the labels. |
| |
| @param labels: iterable of labels. |
| """ |
| labels = (label |
| for label in labels |
| if _is_cros_version(label)) |
| for label in labels: |
| try: |
| cros_image = _CrosImage(_get_cros_version(label)) |
| except ValueError as e: |
| logging.warning('Could not parse cros-version. Error msg: %s', e) |
| self._milestone = 'N/A' |
| else: |
| self._milestone = cros_image.milestone |
| break |
| |
| |
| def _command_line(self): |
| # If we give queue_entry to _autoserv_command_line, then it will append |
| # -c for this invocation if the queue_entry is a client side test. We |
| # don't want that, as it messes with provisioning, so we just drop it |
| # from the arguments here. |
| # Note that we also don't verify job_repo_url as provisioining tasks are |
| # required to stage whatever content we need, and the job itself will |
| # force autotest to be staged if it isn't already. |
| return autoserv_utils._autoserv_command_line(self.host.hostname, |
| self._extra_command_args, |
| in_lab=True) |
| |
| |
| def prolog(self): |
| super(ProvisionTask, self).prolog() |
| # add check for previous provision task and abort if exist. |
| logging.info("starting provision task for host: %s", self.host.hostname) |
| self.queue_entry.set_status( |
| models.HostQueueEntry.Status.PROVISIONING) |
| self.host.set_status(models.Host.Status.PROVISIONING) |
| |
| |
| def epilog(self): |
| super(ProvisionTask, self).epilog() |
| |
| # If we were not successful in provisioning the machine |
| # leave the DUT in whatever status was set in the PreJobTask's |
| # epilog. If this task was successful the host status will get |
| # set appropriately as a fallout of the hqe's on_pending. If |
| # we don't call on_pending, it can only be because: |
| # 1. This task was not successful: |
| # a. Another repair is queued: this repair job will set the host |
| # status, and it will remain in 'Provisioning' till then. |
| # b. We have hit the max_repair_limit: in which case the host |
| # status is set to 'RepairFailed' in the epilog of PreJobTask. |
| # 2. The task was successful, but there are other special tasks: |
| # Those special tasks will set the host status appropriately. |
| if self._should_pending(): |
| self.queue_entry.on_pending() |
| |
| |
| class RepairTask(agent_task.SpecialAgentTask): |
| TASK_TYPE = models.SpecialTask.Task.REPAIR |
| |
| |
| def __init__(self, task): |
| """\ |
| queue_entry: queue entry to mark failed if this repair fails. |
| """ |
| protection = host_protections.Protection.get_string( |
| task.host.protection) |
| # normalize the protection name |
| protection = host_protections.Protection.get_attr_name(protection) |
| |
| args = ['-R', '--host-protection', protection] |
| if task.queue_entry: |
| args.extend(self._generate_autoserv_label_args(task)) |
| |
| super(RepairTask, self).__init__(task, args) |
| |
| # *don't* include the queue entry in IDs -- if the queue entry is |
| # aborted, we want to leave the repair task running |
| self._set_ids(host=self.host) |
| |
| |
| def prolog(self): |
| super(RepairTask, self).prolog() |
| logging.info("repair_task starting") |
| self.host.set_status(models.Host.Status.REPAIRING) |
| |
| |
| def epilog(self): |
| super(RepairTask, self).epilog() |
| |
| if self.success: |
| self.host.set_status(models.Host.Status.READY) |
| else: |
| self.host.set_status(models.Host.Status.REPAIR_FAILED) |
| if self.queue_entry: |
| self._fail_queue_entry() |
| self.host.record_working_state(bool(self.success), |
| self.task.time_finished) |