1*9c5db199SXin Li#!/usr/bin/python3 2*9c5db199SXin Li# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li# This module provides functions for caller to retrieve a job's history, 7*9c5db199SXin Li# including special tasks executed before and after the job, and each steps 8*9c5db199SXin Li# start/end time. 9*9c5db199SXin Li 10*9c5db199SXin Lifrom __future__ import absolute_import 11*9c5db199SXin Lifrom __future__ import division 12*9c5db199SXin Lifrom __future__ import print_function 13*9c5db199SXin Li 14*9c5db199SXin Liimport argparse 15*9c5db199SXin Liimport datetime as datetime_base 16*9c5db199SXin Li 17*9c5db199SXin Liimport common 18*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 19*9c5db199SXin Lifrom autotest_lib.frontend import setup_django_environment 20*9c5db199SXin Lifrom autotest_lib.frontend.afe import models 21*9c5db199SXin Lifrom autotest_lib.frontend.tko import models as tko_models 22*9c5db199SXin Li 23*9c5db199SXin LiCONFIG = global_config.global_config 24*9c5db199SXin LiAUTOTEST_SERVER = CONFIG.get_config_value('SERVER', 'hostname', type=str) 25*9c5db199SXin Li 26*9c5db199SXin LiLOG_BASE_URL = 'http://%s/tko/retrieve_logs.cgi?job=/results/' % AUTOTEST_SERVER 27*9c5db199SXin LiJOB_URL = LOG_BASE_URL + '%(job_id)s-%(owner)s/%(hostname)s' 28*9c5db199SXin LiLOG_PATH_FMT = 'hosts/%(hostname)s/%(task_id)d-%(task_name)s' 29*9c5db199SXin LiTASK_URL = LOG_BASE_URL + LOG_PATH_FMT 30*9c5db199SXin LiAUTOSERV_DEBUG_LOG = 'debug/autoserv.DEBUG' 31*9c5db199SXin Li 32*9c5db199SXin Li# Add some buffer before and after job start/end time when searching for special 33*9c5db199SXin Li# tasks. This is to guarantee to include reset before the job starts and repair 34*9c5db199SXin Li# and cleanup after the job finishes. 35*9c5db199SXin LiTIME_BUFFER = datetime_base.timedelta(hours=2) 36*9c5db199SXin Li 37*9c5db199SXin Li 38*9c5db199SXin Liclass JobHistoryObject(object): 39*9c5db199SXin Li """A common interface to call get_history to return a dictionary of the 40*9c5db199SXin Li object's history record, e.g., start/end time. 41*9c5db199SXin Li """ 42*9c5db199SXin Li 43*9c5db199SXin Li def build_history_entry(self): 44*9c5db199SXin Li """Build a history entry. 45*9c5db199SXin Li 46*9c5db199SXin Li This function expect the object has required attributes. Any missing 47*9c5db199SXin Li attributes will lead to failure. 48*9c5db199SXin Li 49*9c5db199SXin Li @return: A dictionary as the history entry of given job/task. 50*9c5db199SXin Li """ 51*9c5db199SXin Li return {'id': self.id, 52*9c5db199SXin Li 'name': self.name, 53*9c5db199SXin Li 'hostname': self.hostname, 54*9c5db199SXin Li 'status': self.status, 55*9c5db199SXin Li 'log_url': self.log_url, 56*9c5db199SXin Li 'autoserv_log_url': self.autoserv_log_url, 57*9c5db199SXin Li 'start_time': self.start_time, 58*9c5db199SXin Li 'end_time': self.end_time, 59*9c5db199SXin Li 'time_used': self.time_used, 60*9c5db199SXin Li } 61*9c5db199SXin Li 62*9c5db199SXin Li 63*9c5db199SXin Li def get_history(self): 64*9c5db199SXin Li """Return a list of dictionaries of select job/task's history. 65*9c5db199SXin Li """ 66*9c5db199SXin Li raise NotImplementedError('You must override this method in child ' 67*9c5db199SXin Li 'class.') 68*9c5db199SXin Li 69*9c5db199SXin Li 70*9c5db199SXin Liclass SpecialTaskInfo(JobHistoryObject): 71*9c5db199SXin Li """Information of a special task. 72*9c5db199SXin Li 73*9c5db199SXin Li Its properties include: 74*9c5db199SXin Li id: Special task ID. 75*9c5db199SXin Li task: An AFE models.SpecialTask object. 76*9c5db199SXin Li hostname: hostname of the DUT that runs the special task. 77*9c5db199SXin Li log_url: Url to debug log. 78*9c5db199SXin Li autoserv_log_url: Url to the autoserv log. 79*9c5db199SXin Li """ 80*9c5db199SXin Li 81*9c5db199SXin Li def __init__(self, task): 82*9c5db199SXin Li """Constructor 83*9c5db199SXin Li 84*9c5db199SXin Li @param task: An AFE models.SpecialTask object, which has the information 85*9c5db199SXin Li of the special task from database. 86*9c5db199SXin Li """ 87*9c5db199SXin Li # Special task ID 88*9c5db199SXin Li self.id = task.id 89*9c5db199SXin Li # AFE special_task model 90*9c5db199SXin Li self.task = task 91*9c5db199SXin Li self.name = task.task 92*9c5db199SXin Li self.hostname = task.host.hostname 93*9c5db199SXin Li self.status = task.status 94*9c5db199SXin Li 95*9c5db199SXin Li # Link to log 96*9c5db199SXin Li task_info = {'task_id': task.id, 'task_name': task.task.lower(), 97*9c5db199SXin Li 'hostname': self.hostname} 98*9c5db199SXin Li self.log_url = TASK_URL % task_info 99*9c5db199SXin Li self.autoserv_log_url = '%s/%s' % (self.log_url, AUTOSERV_DEBUG_LOG) 100*9c5db199SXin Li 101*9c5db199SXin Li self.start_time = self.task.time_started 102*9c5db199SXin Li self.end_time = self.task.time_finished 103*9c5db199SXin Li if self.start_time and self.end_time: 104*9c5db199SXin Li self.time_used = (self.end_time - self.start_time).total_seconds() 105*9c5db199SXin Li else: 106*9c5db199SXin Li self.time_used = None 107*9c5db199SXin Li 108*9c5db199SXin Li 109*9c5db199SXin Li def __str__(self): 110*9c5db199SXin Li """Get a formatted string of the details of the task info. 111*9c5db199SXin Li """ 112*9c5db199SXin Li return ('Task %d: %s from %s to %s, for %s seconds.\n' % 113*9c5db199SXin Li (self.id, self.task.task, self.start_time, self.end_time, 114*9c5db199SXin Li self.time_used)) 115*9c5db199SXin Li 116*9c5db199SXin Li 117*9c5db199SXin Li def get_history(self): 118*9c5db199SXin Li """Return a dictionary of selected object properties. 119*9c5db199SXin Li """ 120*9c5db199SXin Li return [self.build_history_entry()] 121*9c5db199SXin Li 122*9c5db199SXin Li 123*9c5db199SXin Liclass TaskCacheCollection(dict): 124*9c5db199SXin Li """A cache to hold tasks for multiple hosts. 125*9c5db199SXin Li 126*9c5db199SXin Li It's a dictionary of host_id: TaskCache. 127*9c5db199SXin Li """ 128*9c5db199SXin Li 129*9c5db199SXin Li def try_get(self, host_id, job_id, start_time, end_time): 130*9c5db199SXin Li """Try to get tasks from cache. 131*9c5db199SXin Li 132*9c5db199SXin Li @param host_id: ID of the host. 133*9c5db199SXin Li @param job_id: ID of the test job that's related to the special task. 134*9c5db199SXin Li @param start_time: Start time to search for special task. 135*9c5db199SXin Li @param end_time: End time to search for special task. 136*9c5db199SXin Li @return: The list of special tasks that are related to given host and 137*9c5db199SXin Li Job id. Note that, None means the cache is not available. 138*9c5db199SXin Li However, [] means no special tasks found in cache. 139*9c5db199SXin Li """ 140*9c5db199SXin Li if not host_id in self: 141*9c5db199SXin Li return None 142*9c5db199SXin Li return self[host_id].try_get(job_id, start_time, end_time) 143*9c5db199SXin Li 144*9c5db199SXin Li 145*9c5db199SXin Li def update(self, host_id, start_time, end_time): 146*9c5db199SXin Li """Update the cache of the given host by searching database. 147*9c5db199SXin Li 148*9c5db199SXin Li @param host_id: ID of the host. 149*9c5db199SXin Li @param start_time: Start time to search for special task. 150*9c5db199SXin Li @param end_time: End time to search for special task. 151*9c5db199SXin Li """ 152*9c5db199SXin Li search_start_time = start_time - TIME_BUFFER 153*9c5db199SXin Li search_end_time = end_time + TIME_BUFFER 154*9c5db199SXin Li tasks = models.SpecialTask.objects.filter( 155*9c5db199SXin Li host_id=host_id, 156*9c5db199SXin Li time_started__gte=search_start_time, 157*9c5db199SXin Li time_started__lte=search_end_time) 158*9c5db199SXin Li self[host_id] = TaskCache(tasks, search_start_time, search_end_time) 159*9c5db199SXin Li 160*9c5db199SXin Li 161*9c5db199SXin Liclass TaskCache(object): 162*9c5db199SXin Li """A cache that hold tasks for a host. 163*9c5db199SXin Li """ 164*9c5db199SXin Li 165*9c5db199SXin Li def __init__(self, tasks=[], start_time=None, end_time=None): 166*9c5db199SXin Li """Constructor 167*9c5db199SXin Li """ 168*9c5db199SXin Li self.tasks = tasks 169*9c5db199SXin Li self.start_time = start_time 170*9c5db199SXin Li self.end_time = end_time 171*9c5db199SXin Li 172*9c5db199SXin Li def try_get(self, job_id, start_time, end_time): 173*9c5db199SXin Li """Try to get tasks from cache. 174*9c5db199SXin Li 175*9c5db199SXin Li @param job_id: ID of the test job that's related to the special task. 176*9c5db199SXin Li @param start_time: Start time to search for special task. 177*9c5db199SXin Li @param end_time: End time to search for special task. 178*9c5db199SXin Li @return: The list of special tasks that are related to the job id. 179*9c5db199SXin Li Note that, None means the cache is not available. 180*9c5db199SXin Li However, [] means no special tasks found in cache. 181*9c5db199SXin Li """ 182*9c5db199SXin Li if start_time < self.start_time or end_time > self.end_time: 183*9c5db199SXin Li return None 184*9c5db199SXin Li return [task for task in self.tasks if task.queue_entry and 185*9c5db199SXin Li task.queue_entry.job.id == job_id] 186*9c5db199SXin Li 187*9c5db199SXin Li 188*9c5db199SXin Liclass TestJobInfo(JobHistoryObject): 189*9c5db199SXin Li """Information of a test job 190*9c5db199SXin Li """ 191*9c5db199SXin Li 192*9c5db199SXin Li def __init__(self, hqe, task_caches=None, suite_start_time=None, 193*9c5db199SXin Li suite_end_time=None): 194*9c5db199SXin Li """Constructor 195*9c5db199SXin Li 196*9c5db199SXin Li @param hqe: HostQueueEntry of the job. 197*9c5db199SXin Li @param task_caches: Special tasks that's from a previous query. 198*9c5db199SXin Li @param suite_start_time: Start time of the suite job, default is 199*9c5db199SXin Li None. Used to build special task search cache. 200*9c5db199SXin Li @param suite_end_time: End time of the suite job, default is 201*9c5db199SXin Li None. Used to build special task search cache. 202*9c5db199SXin Li """ 203*9c5db199SXin Li # AFE job ID 204*9c5db199SXin Li self.id = hqe.job.id 205*9c5db199SXin Li # AFE job model 206*9c5db199SXin Li self.job = hqe.job 207*9c5db199SXin Li # Name of the job, strip all build and suite info. 208*9c5db199SXin Li self.name = hqe.job.name.split('/')[-1] 209*9c5db199SXin Li self.status = hqe.status if hqe else None 210*9c5db199SXin Li 211*9c5db199SXin Li try: 212*9c5db199SXin Li self.tko_job = tko_models.Job.objects.filter(afe_job_id=self.id)[0] 213*9c5db199SXin Li self.host = models.Host.objects.filter( 214*9c5db199SXin Li hostname=self.tko_job.machine.hostname)[0] 215*9c5db199SXin Li self.hostname = self.tko_job.machine.hostname 216*9c5db199SXin Li self.start_time = self.tko_job.started_time 217*9c5db199SXin Li self.end_time = self.tko_job.finished_time 218*9c5db199SXin Li except IndexError: 219*9c5db199SXin Li # The test job was never started. 220*9c5db199SXin Li self.tko_job = None 221*9c5db199SXin Li self.host = None 222*9c5db199SXin Li self.hostname = None 223*9c5db199SXin Li self.start_time = None 224*9c5db199SXin Li self.end_time = None 225*9c5db199SXin Li 226*9c5db199SXin Li if self.end_time and self.start_time: 227*9c5db199SXin Li self.time_used = (self.end_time - self.start_time).total_seconds() 228*9c5db199SXin Li else: 229*9c5db199SXin Li self.time_used = None 230*9c5db199SXin Li 231*9c5db199SXin Li # Link to log 232*9c5db199SXin Li self.log_url = JOB_URL % {'job_id': hqe.job.id, 'owner': hqe.job.owner, 233*9c5db199SXin Li 'hostname': self.hostname} 234*9c5db199SXin Li self.autoserv_log_url = '%s/%s' % (self.log_url, AUTOSERV_DEBUG_LOG) 235*9c5db199SXin Li 236*9c5db199SXin Li self._get_special_tasks(hqe, task_caches, suite_start_time, 237*9c5db199SXin Li suite_end_time) 238*9c5db199SXin Li 239*9c5db199SXin Li 240*9c5db199SXin Li def _get_special_tasks(self, hqe, task_caches=None, suite_start_time=None, 241*9c5db199SXin Li suite_end_time=None): 242*9c5db199SXin Li """Get special tasks ran before and after the test job. 243*9c5db199SXin Li 244*9c5db199SXin Li @param hqe: HostQueueEntry of the job. 245*9c5db199SXin Li @param task_caches: Special tasks that's from a previous query. 246*9c5db199SXin Li @param suite_start_time: Start time of the suite job, default is 247*9c5db199SXin Li None. Used to build special task search cache. 248*9c5db199SXin Li @param suite_end_time: End time of the suite job, default is 249*9c5db199SXin Li None. Used to build special task search cache. 250*9c5db199SXin Li """ 251*9c5db199SXin Li # Special tasks run before job starts. 252*9c5db199SXin Li self.tasks_before = [] 253*9c5db199SXin Li # Special tasks run after job finished. 254*9c5db199SXin Li self.tasks_after = [] 255*9c5db199SXin Li 256*9c5db199SXin Li # Skip locating special tasks if hqe is None, or not started yet, as 257*9c5db199SXin Li # that indicates the test job might not be started. 258*9c5db199SXin Li if not hqe or not hqe.started_on: 259*9c5db199SXin Li return 260*9c5db199SXin Li 261*9c5db199SXin Li # Assume special tasks for the test job all start within 2 hours 262*9c5db199SXin Li # before the test job starts or 2 hours after the test finishes. In most 263*9c5db199SXin Li # cases, special task won't take longer than 2 hours to start before 264*9c5db199SXin Li # test job starts and after test job finishes. 265*9c5db199SXin Li search_start_time = hqe.started_on - TIME_BUFFER 266*9c5db199SXin Li search_end_time = (hqe.finished_on + TIME_BUFFER if hqe.finished_on else 267*9c5db199SXin Li hqe.started_on + TIME_BUFFER) 268*9c5db199SXin Li 269*9c5db199SXin Li if task_caches is not None and suite_start_time and suite_end_time: 270*9c5db199SXin Li tasks = task_caches.try_get(self.host.id, self.id, 271*9c5db199SXin Li suite_start_time, suite_end_time) 272*9c5db199SXin Li if tasks is None: 273*9c5db199SXin Li task_caches.update(self.host.id, search_start_time, 274*9c5db199SXin Li search_end_time) 275*9c5db199SXin Li tasks = task_caches.try_get(self.host.id, self.id, 276*9c5db199SXin Li suite_start_time, suite_end_time) 277*9c5db199SXin Li else: 278*9c5db199SXin Li tasks = models.SpecialTask.objects.filter( 279*9c5db199SXin Li host_id=self.host.id, 280*9c5db199SXin Li time_started__gte=search_start_time, 281*9c5db199SXin Li time_started__lte=search_end_time) 282*9c5db199SXin Li tasks = [task for task in tasks if task.queue_entry and 283*9c5db199SXin Li task.queue_entry.job.id == self.id] 284*9c5db199SXin Li 285*9c5db199SXin Li for task in tasks: 286*9c5db199SXin Li task_info = SpecialTaskInfo(task) 287*9c5db199SXin Li if task.time_started < self.start_time: 288*9c5db199SXin Li self.tasks_before.append(task_info) 289*9c5db199SXin Li else: 290*9c5db199SXin Li self.tasks_after.append(task_info) 291*9c5db199SXin Li 292*9c5db199SXin Li 293*9c5db199SXin Li def get_history(self): 294*9c5db199SXin Li """Get the history of a test job. 295*9c5db199SXin Li 296*9c5db199SXin Li @return: A list of special tasks and test job information. 297*9c5db199SXin Li """ 298*9c5db199SXin Li history = [] 299*9c5db199SXin Li history.extend([task.build_history_entry() for task in 300*9c5db199SXin Li self.tasks_before]) 301*9c5db199SXin Li history.append(self.build_history_entry()) 302*9c5db199SXin Li history.extend([task.build_history_entry() for task in 303*9c5db199SXin Li self.tasks_after]) 304*9c5db199SXin Li return history 305*9c5db199SXin Li 306*9c5db199SXin Li 307*9c5db199SXin Li def __str__(self): 308*9c5db199SXin Li """Get a formatted string of the details of the job info. 309*9c5db199SXin Li """ 310*9c5db199SXin Li result = '%d: %s\n' % (self.id, self.name) 311*9c5db199SXin Li for task in self.tasks_before: 312*9c5db199SXin Li result += str(task) 313*9c5db199SXin Li 314*9c5db199SXin Li result += ('Test from %s to %s, for %s seconds.\n' % 315*9c5db199SXin Li (self.start_time, self.end_time, self.time_used)) 316*9c5db199SXin Li 317*9c5db199SXin Li for task in self.tasks_after: 318*9c5db199SXin Li result += str(task) 319*9c5db199SXin Li 320*9c5db199SXin Li return result 321*9c5db199SXin Li 322*9c5db199SXin Li 323*9c5db199SXin Liclass SuiteJobInfo(JobHistoryObject): 324*9c5db199SXin Li """Information of a suite job 325*9c5db199SXin Li """ 326*9c5db199SXin Li 327*9c5db199SXin Li def __init__(self, hqe): 328*9c5db199SXin Li """Constructor 329*9c5db199SXin Li 330*9c5db199SXin Li @param hqe: HostQueueEntry of the job. 331*9c5db199SXin Li """ 332*9c5db199SXin Li # AFE job ID 333*9c5db199SXin Li self.id = hqe.job.id 334*9c5db199SXin Li # AFE job model 335*9c5db199SXin Li self.job = hqe.job 336*9c5db199SXin Li # Name of the job, strip all build and suite info. 337*9c5db199SXin Li self.name = hqe.job.name.split('/')[-1] 338*9c5db199SXin Li self.status = hqe.status if hqe else None 339*9c5db199SXin Li 340*9c5db199SXin Li self.log_url = JOB_URL % {'job_id': hqe.job.id, 'owner': hqe.job.owner, 341*9c5db199SXin Li 'hostname': 'hostless'} 342*9c5db199SXin Li 343*9c5db199SXin Li hqe = models.HostQueueEntry.objects.filter(job_id=hqe.job.id)[0] 344*9c5db199SXin Li self.start_time = hqe.started_on 345*9c5db199SXin Li self.end_time = hqe.finished_on 346*9c5db199SXin Li if self.start_time and self.end_time: 347*9c5db199SXin Li self.time_used = (self.end_time - self.start_time).total_seconds() 348*9c5db199SXin Li else: 349*9c5db199SXin Li self.time_used = None 350*9c5db199SXin Li 351*9c5db199SXin Li # Cache of special tasks, hostname: ((start_time, end_time), [tasks]) 352*9c5db199SXin Li task_caches = TaskCacheCollection() 353*9c5db199SXin Li self.test_jobs = [] 354*9c5db199SXin Li for job in models.Job.objects.filter(parent_job_id=self.id): 355*9c5db199SXin Li try: 356*9c5db199SXin Li job_hqe = models.HostQueueEntry.objects.filter(job_id=job.id)[0] 357*9c5db199SXin Li except IndexError: 358*9c5db199SXin Li continue 359*9c5db199SXin Li self.test_jobs.append(TestJobInfo(job_hqe, task_caches, 360*9c5db199SXin Li self.start_time, self.end_time)) 361*9c5db199SXin Li 362*9c5db199SXin Li 363*9c5db199SXin Li def get_history(self): 364*9c5db199SXin Li """Get the history of a suite job. 365*9c5db199SXin Li 366*9c5db199SXin Li @return: A list of special tasks and test job information that has 367*9c5db199SXin Li suite job as the parent job. 368*9c5db199SXin Li """ 369*9c5db199SXin Li history = [] 370*9c5db199SXin Li for job in sorted(self.test_jobs, 371*9c5db199SXin Li key=lambda j: (j.hostname, j.start_time)): 372*9c5db199SXin Li history.extend(job.get_history()) 373*9c5db199SXin Li return history 374*9c5db199SXin Li 375*9c5db199SXin Li 376*9c5db199SXin Li def __str__(self): 377*9c5db199SXin Li """Get a formatted string of the details of the job info. 378*9c5db199SXin Li """ 379*9c5db199SXin Li result = '%d: %s\n' % (self.id, self.name) 380*9c5db199SXin Li for job in self.test_jobs: 381*9c5db199SXin Li result += str(job) 382*9c5db199SXin Li result += '-' * 80 + '\n' 383*9c5db199SXin Li return result 384*9c5db199SXin Li 385*9c5db199SXin Li 386*9c5db199SXin Lidef get_job_info(job_id): 387*9c5db199SXin Li """Get the history of a job. 388*9c5db199SXin Li 389*9c5db199SXin Li @param job_id: ID of the job. 390*9c5db199SXin Li @return: A TestJobInfo object that contains the test job and its special 391*9c5db199SXin Li tasks' start/end time, if the job is a test job. Otherwise, return 392*9c5db199SXin Li a SuiteJobInfo object if the job is a suite job. 393*9c5db199SXin Li @raise Exception: if the test job can't be found in database. 394*9c5db199SXin Li """ 395*9c5db199SXin Li try: 396*9c5db199SXin Li hqe = models.HostQueueEntry.objects.filter(job_id=job_id)[0] 397*9c5db199SXin Li except IndexError: 398*9c5db199SXin Li raise Exception('No HQE found for job ID %d' % job_id) 399*9c5db199SXin Li 400*9c5db199SXin Li if hqe and hqe.execution_subdir != 'hostless': 401*9c5db199SXin Li return TestJobInfo(hqe) 402*9c5db199SXin Li else: 403*9c5db199SXin Li return SuiteJobInfo(hqe) 404*9c5db199SXin Li 405*9c5db199SXin Li 406*9c5db199SXin Lidef main(): 407*9c5db199SXin Li """Main script. 408*9c5db199SXin Li 409*9c5db199SXin Li The script accepts a job ID and print out the test job and its special 410*9c5db199SXin Li tasks' start/end time. 411*9c5db199SXin Li """ 412*9c5db199SXin Li parser = argparse.ArgumentParser() 413*9c5db199SXin Li parser.add_argument('--job_id', type=int, dest='job_id', required=True) 414*9c5db199SXin Li options = parser.parse_args() 415*9c5db199SXin Li 416*9c5db199SXin Li job_info = get_job_info(options.job_id) 417*9c5db199SXin Li 418*9c5db199SXin Li print(job_info) 419*9c5db199SXin Li 420*9c5db199SXin Li 421*9c5db199SXin Liif __name__ == '__main__': 422*9c5db199SXin Li main() 423