1*9c5db199SXin Li# Copyright 2015 The Chromium OS Authors. All rights reserved. 2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 3*9c5db199SXin Li# found in the LICENSE file. 4*9c5db199SXin Li 5*9c5db199SXin Li"""Services relating to DUT status and job history. 6*9c5db199SXin Li 7*9c5db199SXin LiThe central abstraction of this module is the `HostJobHistory` 8*9c5db199SXin Liclass. This class provides two related pieces of information 9*9c5db199SXin Liregarding a single DUT: 10*9c5db199SXin Li * A history of tests and special tasks that have run on 11*9c5db199SXin Li the DUT in a given time range. 12*9c5db199SXin Li * Whether the DUT was "working" or "broken" at a given 13*9c5db199SXin Li time. 14*9c5db199SXin Li 15*9c5db199SXin LiThe "working" or "broken" status of a DUT is determined by 16*9c5db199SXin Lithe DUT's special task history. At the end of any job or 17*9c5db199SXin Litask, the status is indicated as follows: 18*9c5db199SXin Li * After any successful special task, the DUT is considered 19*9c5db199SXin Li "working". 20*9c5db199SXin Li * After any failed Repair task, the DUT is considered "broken". 21*9c5db199SXin Li * After any other special task or after any regular test job, the 22*9c5db199SXin Li DUT's status is considered unchanged. 23*9c5db199SXin Li 24*9c5db199SXin LiDefinitions for terms used in the code below: 25*9c5db199SXin Li * status task - Any special task that determines the DUT's 26*9c5db199SXin Li status; that is, any successful task, or any failed Repair. 27*9c5db199SXin Li * diagnosis interval - A time interval during which DUT status 28*9c5db199SXin Li changed either from "working" to "broken", or vice versa. The 29*9c5db199SXin Li interval starts with the last status task with the old status, 30*9c5db199SXin Li and ends after the first status task with the new status. 31*9c5db199SXin Li 32*9c5db199SXin LiDiagnosis intervals are interesting because they normally contain 33*9c5db199SXin Lithe logs explaining a failure or repair event. 34*9c5db199SXin Li 35*9c5db199SXin Li""" 36*9c5db199SXin Li 37*9c5db199SXin Liimport common 38*9c5db199SXin Liimport os 39*9c5db199SXin Lifrom autotest_lib.frontend import setup_django_environment 40*9c5db199SXin Lifrom django.db import models as django_models 41*9c5db199SXin Li 42*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 43*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 44*9c5db199SXin Lifrom autotest_lib.client.common_lib import time_utils 45*9c5db199SXin Lifrom autotest_lib.frontend.afe import models as afe_models 46*9c5db199SXin Lifrom autotest_lib.server import constants 47*9c5db199SXin Li 48*9c5db199SXin Li 49*9c5db199SXin Li# Values used to describe the diagnosis of a DUT. These values are 50*9c5db199SXin Li# used to indicate both DUT status after a job or task, and also 51*9c5db199SXin Li# diagnosis of whether the DUT was working at the end of a given 52*9c5db199SXin Li# time interval. 53*9c5db199SXin Li# 54*9c5db199SXin Li# UNUSED: Used when there are no events recorded in a given 55*9c5db199SXin Li# time interval. 56*9c5db199SXin Li# UNKNOWN: For an individual event, indicates that the DUT status 57*9c5db199SXin Li# is unchanged from the previous event. For a time interval, 58*9c5db199SXin Li# indicates that the DUT's status can't be determined from the 59*9c5db199SXin Li# DUT's history. 60*9c5db199SXin Li# WORKING: Indicates that the DUT was working normally after the 61*9c5db199SXin Li# event, or at the end of the time interval. 62*9c5db199SXin Li# BROKEN: Indicates that the DUT needed manual repair after the 63*9c5db199SXin Li# event, or at the end of the time interval. 64*9c5db199SXin Li# 65*9c5db199SXin LiUNUSED = 0 66*9c5db199SXin LiUNKNOWN = 1 67*9c5db199SXin LiWORKING = 2 68*9c5db199SXin LiBROKEN = 3 69*9c5db199SXin Li 70*9c5db199SXin Li 71*9c5db199SXin Listatus_names = { 72*9c5db199SXin Li UNUSED: "UNUSED", 73*9c5db199SXin Li UNKNOWN: "UNKNOWN", 74*9c5db199SXin Li WORKING: "WORKING", 75*9c5db199SXin Li BROKEN: "BROKEN", 76*9c5db199SXin Li} 77*9c5db199SXin Li 78*9c5db199SXin Li 79*9c5db199SXin Lidef parse_time(time_string): 80*9c5db199SXin Li """Parse time according to a canonical form. 81*9c5db199SXin Li 82*9c5db199SXin Li The "canonical" form is the form in which date/time 83*9c5db199SXin Li values are stored in the database. 84*9c5db199SXin Li 85*9c5db199SXin Li @param time_string Time to be parsed. 86*9c5db199SXin Li """ 87*9c5db199SXin Li return int(time_utils.to_epoch_time(time_string)) 88*9c5db199SXin Li 89*9c5db199SXin Li 90*9c5db199SXin Liclass _JobEvent(object): 91*9c5db199SXin Li """Information about an event in host history. 92*9c5db199SXin Li 93*9c5db199SXin Li This remembers the relevant data from a single event in host 94*9c5db199SXin Li history. An event is any change in DUT state caused by a job 95*9c5db199SXin Li or special task. The data captured are the start and end times 96*9c5db199SXin Li of the event, the URL of logs to the job or task causing the 97*9c5db199SXin Li event, and a diagnosis of whether the DUT was working or failed 98*9c5db199SXin Li afterwards. 99*9c5db199SXin Li 100*9c5db199SXin Li This class is an adapter around the database model objects 101*9c5db199SXin Li describing jobs and special tasks. This is an abstract 102*9c5db199SXin Li superclass, with concrete subclasses for `HostQueueEntry` and 103*9c5db199SXin Li `SpecialTask` objects. 104*9c5db199SXin Li 105*9c5db199SXin Li @property start_time Time the job or task began execution. 106*9c5db199SXin Li @property end_time Time the job or task finished execution. 107*9c5db199SXin Li @property id id of the event in the AFE database. 108*9c5db199SXin Li @property name Name of the event, derived from the AFE database. 109*9c5db199SXin Li @property job_status Short string describing the event's final status. 110*9c5db199SXin Li @property logdir Relative path to the logs for the event's job. 111*9c5db199SXin Li @property job_url URL to the logs for the event's job. 112*9c5db199SXin Li @property gs_url GS URL to the logs for the event's job. 113*9c5db199SXin Li @property job_id id of the AFE job for HQEs. None otherwise. 114*9c5db199SXin Li @property diagnosis Working status of the DUT after the event. 115*9c5db199SXin Li @property is_special Boolean indicating if the event is a special task. 116*9c5db199SXin Li 117*9c5db199SXin Li """ 118*9c5db199SXin Li 119*9c5db199SXin Li get_config_value = global_config.global_config.get_config_value 120*9c5db199SXin Li _LOG_URL_PATTERN = ('%s/browse/chromeos-autotest-results/%%s/' 121*9c5db199SXin Li % get_config_value('AUTOTEST_WEB', 'stainless_url', 122*9c5db199SXin Li default=None)) 123*9c5db199SXin Li 124*9c5db199SXin Li @classmethod 125*9c5db199SXin Li def get_gs_url(cls, logdir): 126*9c5db199SXin Li """Return a GS URL to job results. 127*9c5db199SXin Li 128*9c5db199SXin Li The URL is constructed from a base URL determined by the 129*9c5db199SXin Li global config, plus the relative path of the job's log 130*9c5db199SXin Li directory. 131*9c5db199SXin Li 132*9c5db199SXin Li @param logdir Relative path of the results log directory. 133*9c5db199SXin Li 134*9c5db199SXin Li @return A URL to the requested results log. 135*9c5db199SXin Li 136*9c5db199SXin Li """ 137*9c5db199SXin Li return os.path.join(utils.get_offload_gsuri(), logdir) 138*9c5db199SXin Li 139*9c5db199SXin Li 140*9c5db199SXin Li def __init__(self, start_time, end_time): 141*9c5db199SXin Li self.start_time = parse_time(start_time) 142*9c5db199SXin Li self.end_time = parse_time(end_time) 143*9c5db199SXin Li 144*9c5db199SXin Li 145*9c5db199SXin Li def __cmp__(self, other): 146*9c5db199SXin Li """Compare two jobs by their start time. 147*9c5db199SXin Li 148*9c5db199SXin Li This is a standard Python `__cmp__` method to allow sorting 149*9c5db199SXin Li `_JobEvent` objects by their times. 150*9c5db199SXin Li 151*9c5db199SXin Li @param other The `_JobEvent` object to compare to `self`. 152*9c5db199SXin Li 153*9c5db199SXin Li """ 154*9c5db199SXin Li return self.start_time - other.start_time 155*9c5db199SXin Li 156*9c5db199SXin Li 157*9c5db199SXin Li @property 158*9c5db199SXin Li def id(self): 159*9c5db199SXin Li """Return the id of the event in the AFE database.""" 160*9c5db199SXin Li raise NotImplementedError() 161*9c5db199SXin Li 162*9c5db199SXin Li 163*9c5db199SXin Li @property 164*9c5db199SXin Li def name(self): 165*9c5db199SXin Li """Return the name of the event.""" 166*9c5db199SXin Li raise NotImplementedError() 167*9c5db199SXin Li 168*9c5db199SXin Li 169*9c5db199SXin Li @property 170*9c5db199SXin Li def job_status(self): 171*9c5db199SXin Li """Return a short string describing the event's final status.""" 172*9c5db199SXin Li raise NotImplementedError() 173*9c5db199SXin Li 174*9c5db199SXin Li 175*9c5db199SXin Li @property 176*9c5db199SXin Li def logdir(self): 177*9c5db199SXin Li """Return the relative path for this event's job logs.""" 178*9c5db199SXin Li raise NotImplementedError() 179*9c5db199SXin Li 180*9c5db199SXin Li 181*9c5db199SXin Li @property 182*9c5db199SXin Li def job_url(self): 183*9c5db199SXin Li """Return the URL for this event's job logs.""" 184*9c5db199SXin Li return self._LOG_URL_PATTERN % self.logdir 185*9c5db199SXin Li 186*9c5db199SXin Li 187*9c5db199SXin Li @property 188*9c5db199SXin Li def gs_url(self): 189*9c5db199SXin Li """Return the GS URL for this event's job logs.""" 190*9c5db199SXin Li return self.get_gs_url(self.logdir) 191*9c5db199SXin Li 192*9c5db199SXin Li 193*9c5db199SXin Li @property 194*9c5db199SXin Li def job_id(self): 195*9c5db199SXin Li """Return the id of the AFE job for HQEs. None otherwise.""" 196*9c5db199SXin Li raise NotImplementedError() 197*9c5db199SXin Li 198*9c5db199SXin Li 199*9c5db199SXin Li @property 200*9c5db199SXin Li def diagnosis(self): 201*9c5db199SXin Li """Return the status of the DUT after this event. 202*9c5db199SXin Li 203*9c5db199SXin Li The diagnosis is interpreted as follows: 204*9c5db199SXin Li UNKNOWN - The DUT status was the same before and after 205*9c5db199SXin Li the event. 206*9c5db199SXin Li WORKING - The DUT appeared to be working after the event. 207*9c5db199SXin Li BROKEN - The DUT likely required manual intervention 208*9c5db199SXin Li after the event. 209*9c5db199SXin Li 210*9c5db199SXin Li @return A valid diagnosis value. 211*9c5db199SXin Li 212*9c5db199SXin Li """ 213*9c5db199SXin Li raise NotImplementedError() 214*9c5db199SXin Li 215*9c5db199SXin Li 216*9c5db199SXin Li @property 217*9c5db199SXin Li def is_special(self): 218*9c5db199SXin Li """Return if the event is for a special task.""" 219*9c5db199SXin Li raise NotImplementedError() 220*9c5db199SXin Li 221*9c5db199SXin Li 222*9c5db199SXin Liclass _SpecialTaskEvent(_JobEvent): 223*9c5db199SXin Li """`_JobEvent` adapter for special tasks. 224*9c5db199SXin Li 225*9c5db199SXin Li This class wraps the standard `_JobEvent` interface around a row 226*9c5db199SXin Li in the `afe_special_tasks` table. 227*9c5db199SXin Li 228*9c5db199SXin Li """ 229*9c5db199SXin Li 230*9c5db199SXin Li @classmethod 231*9c5db199SXin Li def get_tasks(cls, afe, host_id, start_time, end_time): 232*9c5db199SXin Li """Return special tasks for a host in a given time range. 233*9c5db199SXin Li 234*9c5db199SXin Li Return a list of `_SpecialTaskEvent` objects representing all 235*9c5db199SXin Li special tasks that ran on the given host in the given time 236*9c5db199SXin Li range. The list is ordered as it was returned by the query 237*9c5db199SXin Li (i.e. unordered). 238*9c5db199SXin Li 239*9c5db199SXin Li @param afe Autotest frontend 240*9c5db199SXin Li @param host_id Database host id of the desired host. 241*9c5db199SXin Li @param start_time Start time of the range of interest. 242*9c5db199SXin Li @param end_time End time of the range of interest. 243*9c5db199SXin Li 244*9c5db199SXin Li @return A list of `_SpecialTaskEvent` objects. 245*9c5db199SXin Li 246*9c5db199SXin Li """ 247*9c5db199SXin Li query_start = time_utils.epoch_time_to_date_string(start_time) 248*9c5db199SXin Li query_end = time_utils.epoch_time_to_date_string(end_time) 249*9c5db199SXin Li tasks = afe.get_host_special_tasks( 250*9c5db199SXin Li host_id, 251*9c5db199SXin Li time_started__gte=query_start, 252*9c5db199SXin Li time_finished__lte=query_end, 253*9c5db199SXin Li is_complete=1) 254*9c5db199SXin Li return [cls(t) for t in tasks] 255*9c5db199SXin Li 256*9c5db199SXin Li 257*9c5db199SXin Li @classmethod 258*9c5db199SXin Li def get_status_task(cls, afe, host_id, end_time): 259*9c5db199SXin Li """Return the task indicating a host's status at a given time. 260*9c5db199SXin Li 261*9c5db199SXin Li The task returned determines the status of the DUT; the 262*9c5db199SXin Li diagnosis on the task indicates the diagnosis for the DUT at 263*9c5db199SXin Li the given `end_time`. 264*9c5db199SXin Li 265*9c5db199SXin Li @param afe Autotest frontend 266*9c5db199SXin Li @param host_id Database host id of the desired host. 267*9c5db199SXin Li @param end_time Find status as of this time. 268*9c5db199SXin Li 269*9c5db199SXin Li @return A `_SpecialTaskEvent` object for the requested task, 270*9c5db199SXin Li or `None` if no task was found. 271*9c5db199SXin Li 272*9c5db199SXin Li """ 273*9c5db199SXin Li query_end = time_utils.epoch_time_to_date_string(end_time) 274*9c5db199SXin Li task = afe.get_host_status_task(host_id, query_end) 275*9c5db199SXin Li return cls(task) if task else None 276*9c5db199SXin Li 277*9c5db199SXin Li 278*9c5db199SXin Li def __init__(self, afetask): 279*9c5db199SXin Li self._afetask = afetask 280*9c5db199SXin Li super(_SpecialTaskEvent, self).__init__( 281*9c5db199SXin Li afetask.time_started, afetask.time_finished) 282*9c5db199SXin Li 283*9c5db199SXin Li 284*9c5db199SXin Li @property 285*9c5db199SXin Li def id(self): 286*9c5db199SXin Li return self._afetask.id 287*9c5db199SXin Li 288*9c5db199SXin Li 289*9c5db199SXin Li @property 290*9c5db199SXin Li def name(self): 291*9c5db199SXin Li return self._afetask.task 292*9c5db199SXin Li 293*9c5db199SXin Li 294*9c5db199SXin Li @property 295*9c5db199SXin Li def job_status(self): 296*9c5db199SXin Li if self._afetask.is_aborted: 297*9c5db199SXin Li return 'ABORTED' 298*9c5db199SXin Li elif self._afetask.success: 299*9c5db199SXin Li return 'PASS' 300*9c5db199SXin Li else: 301*9c5db199SXin Li return 'FAIL' 302*9c5db199SXin Li 303*9c5db199SXin Li 304*9c5db199SXin Li @property 305*9c5db199SXin Li def logdir(self): 306*9c5db199SXin Li return ('hosts/%s/%s-%s' % 307*9c5db199SXin Li (self._afetask.host.hostname, self._afetask.id, 308*9c5db199SXin Li self._afetask.task.lower())) 309*9c5db199SXin Li 310*9c5db199SXin Li 311*9c5db199SXin Li @property 312*9c5db199SXin Li def job_id(self): 313*9c5db199SXin Li return None 314*9c5db199SXin Li 315*9c5db199SXin Li 316*9c5db199SXin Li @property 317*9c5db199SXin Li def diagnosis(self): 318*9c5db199SXin Li if self._afetask.success: 319*9c5db199SXin Li return WORKING 320*9c5db199SXin Li elif self._afetask.task == 'Repair': 321*9c5db199SXin Li return BROKEN 322*9c5db199SXin Li else: 323*9c5db199SXin Li return UNKNOWN 324*9c5db199SXin Li 325*9c5db199SXin Li 326*9c5db199SXin Li @property 327*9c5db199SXin Li def is_special(self): 328*9c5db199SXin Li return True 329*9c5db199SXin Li 330*9c5db199SXin Li 331*9c5db199SXin Liclass _TestJobEvent(_JobEvent): 332*9c5db199SXin Li """`_JobEvent` adapter for regular test jobs. 333*9c5db199SXin Li 334*9c5db199SXin Li This class wraps the standard `_JobEvent` interface around a row 335*9c5db199SXin Li in the `afe_host_queue_entries` table. 336*9c5db199SXin Li 337*9c5db199SXin Li """ 338*9c5db199SXin Li 339*9c5db199SXin Li @classmethod 340*9c5db199SXin Li def get_hqes(cls, afe, host_id, start_time, end_time): 341*9c5db199SXin Li """Return HQEs for a host in a given time range. 342*9c5db199SXin Li 343*9c5db199SXin Li Return a list of `_TestJobEvent` objects representing all the 344*9c5db199SXin Li HQEs of all the jobs that ran on the given host in the given 345*9c5db199SXin Li time range. The list is ordered as it was returned by the 346*9c5db199SXin Li query (i.e. unordered). 347*9c5db199SXin Li 348*9c5db199SXin Li @param afe Autotest frontend 349*9c5db199SXin Li @param host_id Database host id of the desired host. 350*9c5db199SXin Li @param start_time Start time of the range of interest. 351*9c5db199SXin Li @param end_time End time of the range of interest. 352*9c5db199SXin Li 353*9c5db199SXin Li @return A list of `_TestJobEvent` objects. 354*9c5db199SXin Li 355*9c5db199SXin Li """ 356*9c5db199SXin Li query_start = time_utils.epoch_time_to_date_string(start_time) 357*9c5db199SXin Li query_end = time_utils.epoch_time_to_date_string(end_time) 358*9c5db199SXin Li hqelist = afe.get_host_queue_entries_by_insert_time( 359*9c5db199SXin Li host_id=host_id, 360*9c5db199SXin Li insert_time_after=query_start, 361*9c5db199SXin Li insert_time_before=query_end, 362*9c5db199SXin Li started_on__gte=query_start, 363*9c5db199SXin Li started_on__lte=query_end, 364*9c5db199SXin Li complete=1) 365*9c5db199SXin Li return [cls(hqe) for hqe in hqelist] 366*9c5db199SXin Li 367*9c5db199SXin Li 368*9c5db199SXin Li def __init__(self, hqe): 369*9c5db199SXin Li self._hqe = hqe 370*9c5db199SXin Li super(_TestJobEvent, self).__init__( 371*9c5db199SXin Li hqe.started_on, hqe.finished_on) 372*9c5db199SXin Li 373*9c5db199SXin Li 374*9c5db199SXin Li @property 375*9c5db199SXin Li def id(self): 376*9c5db199SXin Li return self._hqe.id 377*9c5db199SXin Li 378*9c5db199SXin Li 379*9c5db199SXin Li @property 380*9c5db199SXin Li def name(self): 381*9c5db199SXin Li return self._hqe.job.name 382*9c5db199SXin Li 383*9c5db199SXin Li 384*9c5db199SXin Li @property 385*9c5db199SXin Li def job_status(self): 386*9c5db199SXin Li return self._hqe.status 387*9c5db199SXin Li 388*9c5db199SXin Li 389*9c5db199SXin Li @property 390*9c5db199SXin Li def logdir(self): 391*9c5db199SXin Li return _get_job_logdir(self._hqe.job) 392*9c5db199SXin Li 393*9c5db199SXin Li 394*9c5db199SXin Li @property 395*9c5db199SXin Li def job_id(self): 396*9c5db199SXin Li return self._hqe.job.id 397*9c5db199SXin Li 398*9c5db199SXin Li 399*9c5db199SXin Li @property 400*9c5db199SXin Li def diagnosis(self): 401*9c5db199SXin Li return UNKNOWN 402*9c5db199SXin Li 403*9c5db199SXin Li 404*9c5db199SXin Li @property 405*9c5db199SXin Li def is_special(self): 406*9c5db199SXin Li return False 407*9c5db199SXin Li 408*9c5db199SXin Li 409*9c5db199SXin Liclass HostJobHistory(object): 410*9c5db199SXin Li """Class to query and remember DUT execution and status history. 411*9c5db199SXin Li 412*9c5db199SXin Li This class is responsible for querying the database to determine 413*9c5db199SXin Li the history of a single DUT in a time interval of interest, and 414*9c5db199SXin Li for remembering the query results for reporting. 415*9c5db199SXin Li 416*9c5db199SXin Li @property hostname Host name of the DUT. 417*9c5db199SXin Li @property start_time Start of the requested time interval, as a unix 418*9c5db199SXin Li timestamp (epoch time). 419*9c5db199SXin Li This field may be `None`. 420*9c5db199SXin Li @property end_time End of the requested time interval, as a unix 421*9c5db199SXin Li timestamp (epoch time). 422*9c5db199SXin Li @property _afe Autotest frontend for queries. 423*9c5db199SXin Li @property _host Database host object for the DUT. 424*9c5db199SXin Li @property _history A list of jobs and special tasks that 425*9c5db199SXin Li ran on the DUT in the requested time 426*9c5db199SXin Li interval, ordered in reverse, from latest 427*9c5db199SXin Li to earliest. 428*9c5db199SXin Li 429*9c5db199SXin Li @property _status_interval A list of all the jobs and special 430*9c5db199SXin Li tasks that ran on the DUT in the 431*9c5db199SXin Li last diagnosis interval prior to 432*9c5db199SXin Li `end_time`, ordered from latest to 433*9c5db199SXin Li earliest. 434*9c5db199SXin Li @property _status_diagnosis The DUT's status as of `end_time`. 435*9c5db199SXin Li @property _status_task The DUT's last status task as of 436*9c5db199SXin Li `end_time`. 437*9c5db199SXin Li 438*9c5db199SXin Li """ 439*9c5db199SXin Li 440*9c5db199SXin Li @classmethod 441*9c5db199SXin Li def get_host_history(cls, afe, hostname, start_time, end_time): 442*9c5db199SXin Li """Create a `HostJobHistory` instance for a single host. 443*9c5db199SXin Li 444*9c5db199SXin Li Simple factory method to construct host history from a 445*9c5db199SXin Li hostname. Simply looks up the host in the AFE database, and 446*9c5db199SXin Li passes it to the class constructor. 447*9c5db199SXin Li 448*9c5db199SXin Li @param afe Autotest frontend 449*9c5db199SXin Li @param hostname Name of the host. 450*9c5db199SXin Li @param start_time Start time for the history's time 451*9c5db199SXin Li interval. 452*9c5db199SXin Li @param end_time End time for the history's time interval. 453*9c5db199SXin Li 454*9c5db199SXin Li @return A new `HostJobHistory` instance. 455*9c5db199SXin Li 456*9c5db199SXin Li """ 457*9c5db199SXin Li afehost = afe.get_hosts(hostname=hostname)[0] 458*9c5db199SXin Li return cls(afe, afehost, start_time, end_time) 459*9c5db199SXin Li 460*9c5db199SXin Li 461*9c5db199SXin Li @classmethod 462*9c5db199SXin Li def get_multiple_histories(cls, afe, start_time, end_time, labels=()): 463*9c5db199SXin Li """Create `HostJobHistory` instances for a set of hosts. 464*9c5db199SXin Li 465*9c5db199SXin Li @param afe Autotest frontend 466*9c5db199SXin Li @param start_time Start time for the history's time 467*9c5db199SXin Li interval. 468*9c5db199SXin Li @param end_time End time for the history's time interval. 469*9c5db199SXin Li @param labels type: [str]. AFE labels to constrain the host query. 470*9c5db199SXin Li This option must be non-empty. An unconstrained 471*9c5db199SXin Li search of the DB is too costly. 472*9c5db199SXin Li 473*9c5db199SXin Li @return A list of new `HostJobHistory` instances. 474*9c5db199SXin Li 475*9c5db199SXin Li """ 476*9c5db199SXin Li assert labels, ( 477*9c5db199SXin Li 'Must specify labels for get_multiple_histories. ' 478*9c5db199SXin Li 'Unconstrainted search of the database is prohibitively costly.') 479*9c5db199SXin Li 480*9c5db199SXin Li kwargs = {'multiple_labels': labels} 481*9c5db199SXin Li hosts = afe.get_hosts(**kwargs) 482*9c5db199SXin Li return [cls(afe, h, start_time, end_time) for h in hosts] 483*9c5db199SXin Li 484*9c5db199SXin Li 485*9c5db199SXin Li def __init__(self, afe, afehost, start_time, end_time): 486*9c5db199SXin Li self._afe = afe 487*9c5db199SXin Li self.hostname = afehost.hostname 488*9c5db199SXin Li self.end_time = end_time 489*9c5db199SXin Li self.start_time = start_time 490*9c5db199SXin Li self._host = afehost 491*9c5db199SXin Li # Don't spend time on queries until they're needed. 492*9c5db199SXin Li self._history = None 493*9c5db199SXin Li self._status_interval = None 494*9c5db199SXin Li self._status_diagnosis = None 495*9c5db199SXin Li self._status_task = None 496*9c5db199SXin Li 497*9c5db199SXin Li 498*9c5db199SXin Li def _get_history(self, start_time, end_time): 499*9c5db199SXin Li """Get the list of events for the given interval.""" 500*9c5db199SXin Li newtasks = _SpecialTaskEvent.get_tasks( 501*9c5db199SXin Li self._afe, self._host.id, start_time, end_time) 502*9c5db199SXin Li newhqes = _TestJobEvent.get_hqes( 503*9c5db199SXin Li self._afe, self._host.id, start_time, end_time) 504*9c5db199SXin Li newhistory = newtasks + newhqes 505*9c5db199SXin Li newhistory.sort(reverse=True) 506*9c5db199SXin Li return newhistory 507*9c5db199SXin Li 508*9c5db199SXin Li 509*9c5db199SXin Li def __iter__(self): 510*9c5db199SXin Li if self._history is None: 511*9c5db199SXin Li self._history = self._get_history(self.start_time, 512*9c5db199SXin Li self.end_time) 513*9c5db199SXin Li return self._history.__iter__() 514*9c5db199SXin Li 515*9c5db199SXin Li 516*9c5db199SXin Li def _extract_prefixed_label(self, prefix): 517*9c5db199SXin Li labels = [l for l in self._host.labels 518*9c5db199SXin Li if l.startswith(prefix)] 519*9c5db199SXin Li return labels[0][len(prefix) : ] if labels else None 520*9c5db199SXin Li 521*9c5db199SXin Li 522*9c5db199SXin Li @property 523*9c5db199SXin Li def host(self): 524*9c5db199SXin Li """Return the AFE host object for this history.""" 525*9c5db199SXin Li return self._host 526*9c5db199SXin Li 527*9c5db199SXin Li 528*9c5db199SXin Li @property 529*9c5db199SXin Li def host_model(self): 530*9c5db199SXin Li """Return the model name for this history's DUT.""" 531*9c5db199SXin Li prefix = constants.Labels.MODEL_PREFIX 532*9c5db199SXin Li return self._extract_prefixed_label(prefix) 533*9c5db199SXin Li 534*9c5db199SXin Li 535*9c5db199SXin Li @property 536*9c5db199SXin Li def host_board(self): 537*9c5db199SXin Li """Return the board name for this history's DUT.""" 538*9c5db199SXin Li prefix = constants.Labels.BOARD_PREFIX 539*9c5db199SXin Li return self._extract_prefixed_label(prefix) 540*9c5db199SXin Li 541*9c5db199SXin Li 542*9c5db199SXin Li @property 543*9c5db199SXin Li def host_pool(self): 544*9c5db199SXin Li """Return the pool name for this history's DUT.""" 545*9c5db199SXin Li prefix = constants.Labels.POOL_PREFIX 546*9c5db199SXin Li return self._extract_prefixed_label(prefix) 547*9c5db199SXin Li 548*9c5db199SXin Li 549*9c5db199SXin Li def _init_status_task(self): 550*9c5db199SXin Li """Fill in `self._status_diagnosis` and `_status_task`.""" 551*9c5db199SXin Li if self._status_diagnosis is not None: 552*9c5db199SXin Li return 553*9c5db199SXin Li self._status_task = _SpecialTaskEvent.get_status_task( 554*9c5db199SXin Li self._afe, self._host.id, self.end_time) 555*9c5db199SXin Li if self._status_task is not None: 556*9c5db199SXin Li self._status_diagnosis = self._status_task.diagnosis 557*9c5db199SXin Li else: 558*9c5db199SXin Li self._status_diagnosis = UNKNOWN 559*9c5db199SXin Li 560*9c5db199SXin Li 561*9c5db199SXin Li def _init_status_interval(self): 562*9c5db199SXin Li """Fill in `self._status_interval`.""" 563*9c5db199SXin Li if self._status_interval is not None: 564*9c5db199SXin Li return 565*9c5db199SXin Li self._init_status_task() 566*9c5db199SXin Li self._status_interval = [] 567*9c5db199SXin Li if self._status_task is None: 568*9c5db199SXin Li return 569*9c5db199SXin Li query_end = time_utils.epoch_time_to_date_string(self.end_time) 570*9c5db199SXin Li interval = self._afe.get_host_diagnosis_interval( 571*9c5db199SXin Li self._host.id, query_end, 572*9c5db199SXin Li self._status_diagnosis != WORKING) 573*9c5db199SXin Li if not interval: 574*9c5db199SXin Li return 575*9c5db199SXin Li self._status_interval = self._get_history( 576*9c5db199SXin Li parse_time(interval[0]), 577*9c5db199SXin Li parse_time(interval[1])) 578*9c5db199SXin Li 579*9c5db199SXin Li 580*9c5db199SXin Li def diagnosis_interval(self): 581*9c5db199SXin Li """Find this history's most recent diagnosis interval. 582*9c5db199SXin Li 583*9c5db199SXin Li Returns a list of `_JobEvent` instances corresponding to the 584*9c5db199SXin Li most recent diagnosis interval occurring before this 585*9c5db199SXin Li history's end time. 586*9c5db199SXin Li 587*9c5db199SXin Li The list is returned as with `self._history`, ordered from 588*9c5db199SXin Li most to least recent. 589*9c5db199SXin Li 590*9c5db199SXin Li @return The list of the `_JobEvent`s in the diagnosis 591*9c5db199SXin Li interval. 592*9c5db199SXin Li 593*9c5db199SXin Li """ 594*9c5db199SXin Li self._init_status_interval() 595*9c5db199SXin Li return self._status_interval 596*9c5db199SXin Li 597*9c5db199SXin Li 598*9c5db199SXin Li def last_diagnosis(self): 599*9c5db199SXin Li """Return the diagnosis of whether the DUT is working. 600*9c5db199SXin Li 601*9c5db199SXin Li This searches the DUT's job history, looking for the most 602*9c5db199SXin Li recent status task for the DUT. Return a tuple of 603*9c5db199SXin Li `(diagnosis, task)`. 604*9c5db199SXin Li 605*9c5db199SXin Li The `diagnosis` entry in the tuple is one of these values: 606*9c5db199SXin Li * UNUSED - The host's last status task is older than 607*9c5db199SXin Li `self.start_time`. 608*9c5db199SXin Li * WORKING - The DUT is working. 609*9c5db199SXin Li * BROKEN - The DUT likely requires manual intervention. 610*9c5db199SXin Li * UNKNOWN - No task could be found indicating status for 611*9c5db199SXin Li the DUT. 612*9c5db199SXin Li 613*9c5db199SXin Li If the DUT was working at last check, but hasn't been used 614*9c5db199SXin Li inside this history's time interval, the status `UNUSED` is 615*9c5db199SXin Li returned with the last status task, instead of `WORKING`. 616*9c5db199SXin Li 617*9c5db199SXin Li The `task` entry in the tuple is the status task that led to 618*9c5db199SXin Li the diagnosis. The task will be `None` if the diagnosis is 619*9c5db199SXin Li `UNKNOWN`. 620*9c5db199SXin Li 621*9c5db199SXin Li @return A tuple with the DUT's diagnosis and the task that 622*9c5db199SXin Li determined it. 623*9c5db199SXin Li 624*9c5db199SXin Li """ 625*9c5db199SXin Li self._init_status_task() 626*9c5db199SXin Li diagnosis = self._status_diagnosis 627*9c5db199SXin Li if (self.start_time is not None and 628*9c5db199SXin Li self._status_task is not None and 629*9c5db199SXin Li self._status_task.end_time < self.start_time and 630*9c5db199SXin Li diagnosis == WORKING): 631*9c5db199SXin Li diagnosis = UNUSED 632*9c5db199SXin Li return diagnosis, self._status_task 633*9c5db199SXin Li 634*9c5db199SXin Li 635*9c5db199SXin Lidef get_diagnosis_interval(host_id, end_time, success): 636*9c5db199SXin Li """Return the last diagnosis interval for a given host and time. 637*9c5db199SXin Li 638*9c5db199SXin Li This routine queries the database for the special tasks on a 639*9c5db199SXin Li given host before a given time. From those tasks it selects the 640*9c5db199SXin Li last status task before a change in status, and the first status 641*9c5db199SXin Li task after the change. When `success` is true, the change must 642*9c5db199SXin Li be from "working" to "broken". When false, the search is for a 643*9c5db199SXin Li change in the opposite direction. 644*9c5db199SXin Li 645*9c5db199SXin Li A "successful status task" is any successful special task. A 646*9c5db199SXin Li "failed status task" is a failed Repair task. These criteria 647*9c5db199SXin Li are based on the definition of "status task" in the module-level 648*9c5db199SXin Li docstring, above. 649*9c5db199SXin Li 650*9c5db199SXin Li This is the RPC endpoint for `AFE.get_host_diagnosis_interval()`. 651*9c5db199SXin Li 652*9c5db199SXin Li @param host_id Database host id of the desired host. 653*9c5db199SXin Li @param end_time Find the last eligible interval before this time. 654*9c5db199SXin Li @param success Whether the eligible interval should start with a 655*9c5db199SXin Li success or a failure. 656*9c5db199SXin Li 657*9c5db199SXin Li @return A list containing the start time of the earliest job 658*9c5db199SXin Li selected, and the end time of the latest job. 659*9c5db199SXin Li 660*9c5db199SXin Li """ 661*9c5db199SXin Li base_query = afe_models.SpecialTask.objects.filter( 662*9c5db199SXin Li host_id=host_id, is_complete=True) 663*9c5db199SXin Li success_query = base_query.filter(success=True) 664*9c5db199SXin Li failure_query = base_query.filter(success=False, task='Repair') 665*9c5db199SXin Li if success: 666*9c5db199SXin Li query0 = success_query 667*9c5db199SXin Li query1 = failure_query 668*9c5db199SXin Li else: 669*9c5db199SXin Li query0 = failure_query 670*9c5db199SXin Li query1 = success_query 671*9c5db199SXin Li query0 = query0.filter(time_finished__lte=end_time) 672*9c5db199SXin Li query0 = query0.order_by('time_started').reverse() 673*9c5db199SXin Li if not query0: 674*9c5db199SXin Li return [] 675*9c5db199SXin Li task0 = query0[0] 676*9c5db199SXin Li query1 = query1.filter(time_finished__gt=task0.time_finished) 677*9c5db199SXin Li task1 = query1.order_by('time_started')[0] 678*9c5db199SXin Li return [task0.time_started.strftime(time_utils.TIME_FMT), 679*9c5db199SXin Li task1.time_finished.strftime(time_utils.TIME_FMT)] 680*9c5db199SXin Li 681*9c5db199SXin Li 682*9c5db199SXin Lidef get_status_task(host_id, end_time): 683*9c5db199SXin Li """Get the last status task for a host before a given time. 684*9c5db199SXin Li 685*9c5db199SXin Li This routine returns a Django query for the AFE database to find 686*9c5db199SXin Li the last task that finished on the given host before the given 687*9c5db199SXin Li time that was either a successful task, or a Repair task. The 688*9c5db199SXin Li query criteria are based on the definition of "status task" in 689*9c5db199SXin Li the module-level docstring, above. 690*9c5db199SXin Li 691*9c5db199SXin Li This is the RPC endpoint for `_SpecialTaskEvent.get_status_task()`. 692*9c5db199SXin Li 693*9c5db199SXin Li @param host_id Database host id of the desired host. 694*9c5db199SXin Li @param end_time End time of the range of interest. 695*9c5db199SXin Li 696*9c5db199SXin Li @return A Django query-set selecting the single special task of 697*9c5db199SXin Li interest. 698*9c5db199SXin Li 699*9c5db199SXin Li """ 700*9c5db199SXin Li # Selects status tasks: any Repair task, or any successful task. 701*9c5db199SXin Li status_tasks = (django_models.Q(task='Repair') | 702*9c5db199SXin Li django_models.Q(success=True)) 703*9c5db199SXin Li # Our caller needs a Django query set in order to serialize the 704*9c5db199SXin Li # result, so we don't resolve the query here; we just return a 705*9c5db199SXin Li # slice with at most one element. 706*9c5db199SXin Li return afe_models.SpecialTask.objects.filter( 707*9c5db199SXin Li status_tasks, 708*9c5db199SXin Li host_id=host_id, 709*9c5db199SXin Li time_finished__lte=end_time, 710*9c5db199SXin Li is_complete=True).order_by('time_started').reverse()[0:1] 711*9c5db199SXin Li 712*9c5db199SXin Li 713*9c5db199SXin Lidef _get_job_logdir(job): 714*9c5db199SXin Li """Gets the logdir for an AFE job. 715*9c5db199SXin Li 716*9c5db199SXin Li @param job Job object which has id and owner properties. 717*9c5db199SXin Li 718*9c5db199SXin Li @return Relative path of the results log directory. 719*9c5db199SXin Li """ 720*9c5db199SXin Li return '%s-%s' % (job.id, job.owner) 721*9c5db199SXin Li 722*9c5db199SXin Li 723*9c5db199SXin Lidef get_job_gs_url(job): 724*9c5db199SXin Li """Gets the GS URL for an AFE job. 725*9c5db199SXin Li 726*9c5db199SXin Li @param job Job object which has id and owner properties. 727*9c5db199SXin Li 728*9c5db199SXin Li @return Absolute GS URL to the results log directory. 729*9c5db199SXin Li """ 730*9c5db199SXin Li return _JobEvent.get_gs_url(_get_job_logdir(job)) 731