| #!/usr/bin/env python |
| |
| import common |
| import json |
| import re |
| import sys |
| |
| from autotest_lib.client.common_lib import time_utils |
| from autotest_lib.server import frontend |
| from autotest_lib.server.lib import status_history |
| from autotest_lib.server.lib import suite_report |
| from chromite.lib import cidb |
| from chromite.lib import commandline |
| from chromite.lib import cros_logging as logging |
| |
| HostJobHistory = status_history.HostJobHistory |
| |
| |
| def GetParser(): |
| """Creates the argparse parser.""" |
| parser = commandline.ArgumentParser(description=__doc__) |
| parser.add_argument('--input', type=str, action='store', |
| help='Input JSON file') |
| parser.add_argument('--output', type=str, action='store', |
| help='Output JSON file') |
| parser.add_argument('--name_filter', type=str, action='store', |
| help='Name of task to look for') |
| parser.add_argument('--status_filter', type=str, action='store', |
| help='Status fo task to look for') |
| parser.add_argument('--afe', type=str, action='store', |
| help='AFE server to connect to') |
| parser.add_argument('suite_ids', type=str, nargs='*', action='store', |
| help='Suite ids to resolve') |
| return parser |
| |
| |
| def GetSuiteHQEs(suite_job_id, look_past_seconds, afe=None, tko=None): |
| """Get the host queue entries for active DUTs during a suite job. |
| |
| @param suite_job_id: Suite's AFE job id. |
| @param look_past_seconds: Number of seconds past the end of the suite |
| job to look for next HQEs. |
| @param afe: AFE database handle. |
| @param tko: TKO database handle. |
| |
| @returns A dictionary keyed on hostname to a list of host queue entry |
| dictionaries. HQE dictionary contains the following keys: |
| name, hostname, job_status, job_url, gs_url, start_time, end_time |
| """ |
| if afe is None: |
| afe = frontend.AFE() |
| if tko is None: |
| tko = frontend.TKO() |
| |
| # Find the suite job and when it ran. |
| statuses = tko.get_job_test_statuses_from_db(suite_job_id) |
| if len(statuses): |
| for s in statuses: |
| if s.test_started_time == 'None' or s.test_finished_time == 'None': |
| logging.error( |
| 'TKO entry missing time: %s %s %s %s %s %s %s %s %s' % |
| (s.id, s.test_name, s.status, s.reason, |
| s.test_started_time, s.test_finished_time, |
| s.job_owner, s.hostname, s.job_tag)) |
| start_time = min(int(time_utils.to_epoch_time(s.test_started_time)) |
| for s in statuses if s.test_started_time != 'None') |
| finish_time = max(int(time_utils.to_epoch_time( |
| s.test_finished_time)) for s in statuses |
| if s.test_finished_time != 'None') |
| else: |
| start_time = None |
| finish_time = None |
| |
| # If there is no start time or finish time, won't be able to get HQEs. |
| if start_time is None or finish_time is None: |
| return {} |
| |
| # Find all the HQE entries. |
| child_jobs = afe.get_jobs(parent_job_id=suite_job_id) |
| child_job_ids = {j.id for j in child_jobs} |
| hqes = afe.get_host_queue_entries(job_id__in=list(child_job_ids)) |
| hostnames = {h.host.hostname for h in hqes if h.host} |
| host_hqes = {} |
| for hostname in hostnames: |
| history = HostJobHistory.get_host_history(afe, hostname, |
| start_time, |
| finish_time + |
| look_past_seconds) |
| for h in history: |
| gs_url = re.sub(r'http://.*/tko/retrieve_logs.cgi\?job=/results', |
| r'gs://chromeos-autotest-results', |
| h.job_url) |
| entry = { |
| 'name': h.name, |
| 'hostname': history.hostname, |
| 'job_status': h.job_status, |
| 'job_url': h.job_url, |
| 'gs_url': gs_url, |
| 'start_time': h.start_time, |
| 'end_time': h.end_time, |
| } |
| host_hqes.setdefault(history.hostname, []).append(entry) |
| |
| return host_hqes |
| |
| |
| def FindSpecialTasks(suite_job_id, look_past_seconds=1800, |
| name_filter=None, status_filter=None, afe=None, tko=None): |
| """Find special tasks that happened around a suite job. |
| |
| @param suite_job_id: Suite's AFE job id. |
| @param look_past_seconds: Number of seconds past the end of the suite |
| job to look for next HQEs. |
| @param name_filter: If not None, only return tasks with this name. |
| @param status_filter: If not None, only return tasks with this status. |
| @param afe: AFE database handle. |
| @param tko: TKO database handle. |
| |
| @returns A dictionary keyed on hostname to a list of host queue entry |
| dictionaries. HQE dictionary contains the following keys: |
| name, hostname, job_status, job_url, gs_url, start_time, end_time, |
| next_entry |
| """ |
| host_hqes = GetSuiteHQEs(suite_job_id, look_past_seconds=look_past_seconds, |
| afe=afe, tko=tko) |
| |
| task_entries = [] |
| for hostname in host_hqes: |
| host_hqes[hostname] = sorted(host_hqes[hostname], |
| key=lambda k: k['start_time']) |
| current = None |
| for e in host_hqes[hostname]: |
| # Check if there is an entry to finish off by adding a pointer |
| # to this new entry. |
| if current: |
| logging.debug(' next task: %(name)s %(job_status)s ' |
| '%(gs_url)s %(start_time)s %(end_time)s' % e) |
| # Only record a pointer to the next entry if filtering some out. |
| if name_filter or status_filter: |
| current['next_entry'] = e |
| task_entries.append(current) |
| current = None |
| |
| # Perform matching. |
| if ((name_filter and e['name'] != name_filter) or |
| (status_filter and e['job_status'] != status_filter)): |
| continue |
| |
| # Instead of appending right away, wait until the next entry |
| # to add a point to it. |
| current = e |
| logging.debug('Task %(name)s: %(job_status)s %(hostname)s ' |
| '%(gs_url)s %(start_time)s %(end_time)s' % e) |
| |
| # Add the last one even if a next entry wasn't found. |
| if current: |
| task_entries.append(current) |
| |
| return task_entries |
| |
| def main(argv): |
| parser = GetParser() |
| options = parser.parse_args(argv) |
| |
| afe = None |
| if options.afe: |
| afe = frontend.AFE(server=options.afe) |
| tko = frontend.TKO() |
| |
| special_tasks = [] |
| builds = [] |
| |
| # Handle a JSON file being specified. |
| if options.input: |
| with open(options.input) as f: |
| data = json.load(f) |
| for build in data.get('builds', []): |
| # For each build, amend it to include the list of |
| # special tasks for its suite's jobs. |
| build.setdefault('special_tasks', {}) |
| for suite_job_id in build['suite_ids']: |
| suite_tasks = FindSpecialTasks( |
| suite_job_id, name_filter=options.name_filter, |
| status_filter=options.status_filter, |
| afe=afe, tko=tko) |
| special_tasks.extend(suite_tasks) |
| build['special_tasks'][suite_job_id] = suite_tasks |
| logging.debug(build) |
| builds.append(build) |
| |
| # Handle and specifically specified suite IDs. |
| for suite_job_id in options.suite_ids: |
| special_tasks.extend(FindSpecialTasks( |
| suite_job_id, name_filter=options.name_filter, |
| status_filter=options.status_filter, afe=afe, tko=tko)) |
| |
| # Output a resulting JSON file. |
| with open(options.output, 'w') if options.output else sys.stdout as f: |
| output = { |
| 'special_tasks': special_tasks, |
| 'name_filter': options.name_filter, |
| 'status_filter': options.status_filter, |
| } |
| if len(builds): |
| output['builds'] = builds |
| json.dump(output, f) |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |