| #!/usr/bin/python3 |
| # Copyright 2014 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # This module provides functions for caller to retrieve a job's history, |
| # including special tasks executed before and after the job, and each steps |
| # start/end time. |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import argparse |
| import datetime as datetime_base |
| |
| import common |
| from autotest_lib.client.common_lib import global_config |
| from autotest_lib.frontend import setup_django_environment |
| from autotest_lib.frontend.afe import models |
| from autotest_lib.frontend.tko import models as tko_models |
| |
| CONFIG = global_config.global_config |
| AUTOTEST_SERVER = CONFIG.get_config_value('SERVER', 'hostname', type=str) |
| |
| LOG_BASE_URL = 'http://%s/tko/retrieve_logs.cgi?job=/results/' % AUTOTEST_SERVER |
| JOB_URL = LOG_BASE_URL + '%(job_id)s-%(owner)s/%(hostname)s' |
| LOG_PATH_FMT = 'hosts/%(hostname)s/%(task_id)d-%(task_name)s' |
| TASK_URL = LOG_BASE_URL + LOG_PATH_FMT |
| AUTOSERV_DEBUG_LOG = 'debug/autoserv.DEBUG' |
| |
| # Add some buffer before and after job start/end time when searching for special |
| # tasks. This is to guarantee to include reset before the job starts and repair |
| # and cleanup after the job finishes. |
| TIME_BUFFER = datetime_base.timedelta(hours=2) |
| |
| |
| class JobHistoryObject(object): |
| """A common interface to call get_history to return a dictionary of the |
| object's history record, e.g., start/end time. |
| """ |
| |
| def build_history_entry(self): |
| """Build a history entry. |
| |
| This function expect the object has required attributes. Any missing |
| attributes will lead to failure. |
| |
| @return: A dictionary as the history entry of given job/task. |
| """ |
| return {'id': self.id, |
| 'name': self.name, |
| 'hostname': self.hostname, |
| 'status': self.status, |
| 'log_url': self.log_url, |
| 'autoserv_log_url': self.autoserv_log_url, |
| 'start_time': self.start_time, |
| 'end_time': self.end_time, |
| 'time_used': self.time_used, |
| } |
| |
| |
| def get_history(self): |
| """Return a list of dictionaries of select job/task's history. |
| """ |
| raise NotImplementedError('You must override this method in child ' |
| 'class.') |
| |
| |
| class SpecialTaskInfo(JobHistoryObject): |
| """Information of a special task. |
| |
| Its properties include: |
| id: Special task ID. |
| task: An AFE models.SpecialTask object. |
| hostname: hostname of the DUT that runs the special task. |
| log_url: Url to debug log. |
| autoserv_log_url: Url to the autoserv log. |
| """ |
| |
| def __init__(self, task): |
| """Constructor |
| |
| @param task: An AFE models.SpecialTask object, which has the information |
| of the special task from database. |
| """ |
| # Special task ID |
| self.id = task.id |
| # AFE special_task model |
| self.task = task |
| self.name = task.task |
| self.hostname = task.host.hostname |
| self.status = task.status |
| |
| # Link to log |
| task_info = {'task_id': task.id, 'task_name': task.task.lower(), |
| 'hostname': self.hostname} |
| self.log_url = TASK_URL % task_info |
| self.autoserv_log_url = '%s/%s' % (self.log_url, AUTOSERV_DEBUG_LOG) |
| |
| self.start_time = self.task.time_started |
| self.end_time = self.task.time_finished |
| if self.start_time and self.end_time: |
| self.time_used = (self.end_time - self.start_time).total_seconds() |
| else: |
| self.time_used = None |
| |
| |
| def __str__(self): |
| """Get a formatted string of the details of the task info. |
| """ |
| return ('Task %d: %s from %s to %s, for %s seconds.\n' % |
| (self.id, self.task.task, self.start_time, self.end_time, |
| self.time_used)) |
| |
| |
| def get_history(self): |
| """Return a dictionary of selected object properties. |
| """ |
| return [self.build_history_entry()] |
| |
| |
| class TaskCacheCollection(dict): |
| """A cache to hold tasks for multiple hosts. |
| |
| It's a dictionary of host_id: TaskCache. |
| """ |
| |
| def try_get(self, host_id, job_id, start_time, end_time): |
| """Try to get tasks from cache. |
| |
| @param host_id: ID of the host. |
| @param job_id: ID of the test job that's related to the special task. |
| @param start_time: Start time to search for special task. |
| @param end_time: End time to search for special task. |
| @return: The list of special tasks that are related to given host and |
| Job id. Note that, None means the cache is not available. |
| However, [] means no special tasks found in cache. |
| """ |
| if not host_id in self: |
| return None |
| return self[host_id].try_get(job_id, start_time, end_time) |
| |
| |
| def update(self, host_id, start_time, end_time): |
| """Update the cache of the given host by searching database. |
| |
| @param host_id: ID of the host. |
| @param start_time: Start time to search for special task. |
| @param end_time: End time to search for special task. |
| """ |
| search_start_time = start_time - TIME_BUFFER |
| search_end_time = end_time + TIME_BUFFER |
| tasks = models.SpecialTask.objects.filter( |
| host_id=host_id, |
| time_started__gte=search_start_time, |
| time_started__lte=search_end_time) |
| self[host_id] = TaskCache(tasks, search_start_time, search_end_time) |
| |
| |
| class TaskCache(object): |
| """A cache that hold tasks for a host. |
| """ |
| |
| def __init__(self, tasks=[], start_time=None, end_time=None): |
| """Constructor |
| """ |
| self.tasks = tasks |
| self.start_time = start_time |
| self.end_time = end_time |
| |
| def try_get(self, job_id, start_time, end_time): |
| """Try to get tasks from cache. |
| |
| @param job_id: ID of the test job that's related to the special task. |
| @param start_time: Start time to search for special task. |
| @param end_time: End time to search for special task. |
| @return: The list of special tasks that are related to the job id. |
| Note that, None means the cache is not available. |
| However, [] means no special tasks found in cache. |
| """ |
| if start_time < self.start_time or end_time > self.end_time: |
| return None |
| return [task for task in self.tasks if task.queue_entry and |
| task.queue_entry.job.id == job_id] |
| |
| |
| class TestJobInfo(JobHistoryObject): |
| """Information of a test job |
| """ |
| |
| def __init__(self, hqe, task_caches=None, suite_start_time=None, |
| suite_end_time=None): |
| """Constructor |
| |
| @param hqe: HostQueueEntry of the job. |
| @param task_caches: Special tasks that's from a previous query. |
| @param suite_start_time: Start time of the suite job, default is |
| None. Used to build special task search cache. |
| @param suite_end_time: End time of the suite job, default is |
| None. Used to build special task search cache. |
| """ |
| # AFE job ID |
| self.id = hqe.job.id |
| # AFE job model |
| self.job = hqe.job |
| # Name of the job, strip all build and suite info. |
| self.name = hqe.job.name.split('/')[-1] |
| self.status = hqe.status if hqe else None |
| |
| try: |
| self.tko_job = tko_models.Job.objects.filter(afe_job_id=self.id)[0] |
| self.host = models.Host.objects.filter( |
| hostname=self.tko_job.machine.hostname)[0] |
| self.hostname = self.tko_job.machine.hostname |
| self.start_time = self.tko_job.started_time |
| self.end_time = self.tko_job.finished_time |
| except IndexError: |
| # The test job was never started. |
| self.tko_job = None |
| self.host = None |
| self.hostname = None |
| self.start_time = None |
| self.end_time = None |
| |
| if self.end_time and self.start_time: |
| self.time_used = (self.end_time - self.start_time).total_seconds() |
| else: |
| self.time_used = None |
| |
| # Link to log |
| self.log_url = JOB_URL % {'job_id': hqe.job.id, 'owner': hqe.job.owner, |
| 'hostname': self.hostname} |
| self.autoserv_log_url = '%s/%s' % (self.log_url, AUTOSERV_DEBUG_LOG) |
| |
| self._get_special_tasks(hqe, task_caches, suite_start_time, |
| suite_end_time) |
| |
| |
| def _get_special_tasks(self, hqe, task_caches=None, suite_start_time=None, |
| suite_end_time=None): |
| """Get special tasks ran before and after the test job. |
| |
| @param hqe: HostQueueEntry of the job. |
| @param task_caches: Special tasks that's from a previous query. |
| @param suite_start_time: Start time of the suite job, default is |
| None. Used to build special task search cache. |
| @param suite_end_time: End time of the suite job, default is |
| None. Used to build special task search cache. |
| """ |
| # Special tasks run before job starts. |
| self.tasks_before = [] |
| # Special tasks run after job finished. |
| self.tasks_after = [] |
| |
| # Skip locating special tasks if hqe is None, or not started yet, as |
| # that indicates the test job might not be started. |
| if not hqe or not hqe.started_on: |
| return |
| |
| # Assume special tasks for the test job all start within 2 hours |
| # before the test job starts or 2 hours after the test finishes. In most |
| # cases, special task won't take longer than 2 hours to start before |
| # test job starts and after test job finishes. |
| search_start_time = hqe.started_on - TIME_BUFFER |
| search_end_time = (hqe.finished_on + TIME_BUFFER if hqe.finished_on else |
| hqe.started_on + TIME_BUFFER) |
| |
| if task_caches is not None and suite_start_time and suite_end_time: |
| tasks = task_caches.try_get(self.host.id, self.id, |
| suite_start_time, suite_end_time) |
| if tasks is None: |
| task_caches.update(self.host.id, search_start_time, |
| search_end_time) |
| tasks = task_caches.try_get(self.host.id, self.id, |
| suite_start_time, suite_end_time) |
| else: |
| tasks = models.SpecialTask.objects.filter( |
| host_id=self.host.id, |
| time_started__gte=search_start_time, |
| time_started__lte=search_end_time) |
| tasks = [task for task in tasks if task.queue_entry and |
| task.queue_entry.job.id == self.id] |
| |
| for task in tasks: |
| task_info = SpecialTaskInfo(task) |
| if task.time_started < self.start_time: |
| self.tasks_before.append(task_info) |
| else: |
| self.tasks_after.append(task_info) |
| |
| |
| def get_history(self): |
| """Get the history of a test job. |
| |
| @return: A list of special tasks and test job information. |
| """ |
| history = [] |
| history.extend([task.build_history_entry() for task in |
| self.tasks_before]) |
| history.append(self.build_history_entry()) |
| history.extend([task.build_history_entry() for task in |
| self.tasks_after]) |
| return history |
| |
| |
| def __str__(self): |
| """Get a formatted string of the details of the job info. |
| """ |
| result = '%d: %s\n' % (self.id, self.name) |
| for task in self.tasks_before: |
| result += str(task) |
| |
| result += ('Test from %s to %s, for %s seconds.\n' % |
| (self.start_time, self.end_time, self.time_used)) |
| |
| for task in self.tasks_after: |
| result += str(task) |
| |
| return result |
| |
| |
| class SuiteJobInfo(JobHistoryObject): |
| """Information of a suite job |
| """ |
| |
| def __init__(self, hqe): |
| """Constructor |
| |
| @param hqe: HostQueueEntry of the job. |
| """ |
| # AFE job ID |
| self.id = hqe.job.id |
| # AFE job model |
| self.job = hqe.job |
| # Name of the job, strip all build and suite info. |
| self.name = hqe.job.name.split('/')[-1] |
| self.status = hqe.status if hqe else None |
| |
| self.log_url = JOB_URL % {'job_id': hqe.job.id, 'owner': hqe.job.owner, |
| 'hostname': 'hostless'} |
| |
| hqe = models.HostQueueEntry.objects.filter(job_id=hqe.job.id)[0] |
| self.start_time = hqe.started_on |
| self.end_time = hqe.finished_on |
| if self.start_time and self.end_time: |
| self.time_used = (self.end_time - self.start_time).total_seconds() |
| else: |
| self.time_used = None |
| |
| # Cache of special tasks, hostname: ((start_time, end_time), [tasks]) |
| task_caches = TaskCacheCollection() |
| self.test_jobs = [] |
| for job in models.Job.objects.filter(parent_job_id=self.id): |
| try: |
| job_hqe = models.HostQueueEntry.objects.filter(job_id=job.id)[0] |
| except IndexError: |
| continue |
| self.test_jobs.append(TestJobInfo(job_hqe, task_caches, |
| self.start_time, self.end_time)) |
| |
| |
| def get_history(self): |
| """Get the history of a suite job. |
| |
| @return: A list of special tasks and test job information that has |
| suite job as the parent job. |
| """ |
| history = [] |
| for job in sorted(self.test_jobs, |
| key=lambda j: (j.hostname, j.start_time)): |
| history.extend(job.get_history()) |
| return history |
| |
| |
| def __str__(self): |
| """Get a formatted string of the details of the job info. |
| """ |
| result = '%d: %s\n' % (self.id, self.name) |
| for job in self.test_jobs: |
| result += str(job) |
| result += '-' * 80 + '\n' |
| return result |
| |
| |
| def get_job_info(job_id): |
| """Get the history of a job. |
| |
| @param job_id: ID of the job. |
| @return: A TestJobInfo object that contains the test job and its special |
| tasks' start/end time, if the job is a test job. Otherwise, return |
| a SuiteJobInfo object if the job is a suite job. |
| @raise Exception: if the test job can't be found in database. |
| """ |
| try: |
| hqe = models.HostQueueEntry.objects.filter(job_id=job_id)[0] |
| except IndexError: |
| raise Exception('No HQE found for job ID %d' % job_id) |
| |
| if hqe and hqe.execution_subdir != 'hostless': |
| return TestJobInfo(hqe) |
| else: |
| return SuiteJobInfo(hqe) |
| |
| |
| def main(): |
| """Main script. |
| |
| The script accepts a job ID and print out the test job and its special |
| tasks' start/end time. |
| """ |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--job_id', type=int, dest='job_id', required=True) |
| options = parser.parse_args() |
| |
| job_info = get_job_info(options.job_id) |
| |
| print(job_info) |
| |
| |
| if __name__ == '__main__': |
| main() |