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