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