xref: /aosp_15_r20/external/autotest/server/lib/status_history.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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