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