You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
472 lines
17 KiB
472 lines
17 KiB
#!/usr/bin/env python2
|
|
# Copyright 2014 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""Report whether DUTs are working or broken.
|
|
|
|
usage: dut_status [ <options> ] [hostname ...]
|
|
|
|
Reports on the history and status of selected DUT hosts, to
|
|
determine whether they're "working" or "broken". For purposes of
|
|
the script, "broken" means "the DUT requires manual intervention
|
|
before it can be used for further testing", and "working" means "not
|
|
broken". The status determination is based on the history of
|
|
completed jobs for the DUT in a given time interval; still-running
|
|
jobs are not considered.
|
|
|
|
Time Interval Selection
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
A DUT's reported status is based on the DUT's job history in a time
|
|
interval determined by command line options. The interval is
|
|
specified with up to two of three options:
|
|
--until/-u DATE/TIME - Specifies an end time for the search
|
|
range. (default: now)
|
|
--since/-s DATE/TIME - Specifies a start time for the search
|
|
range. (no default)
|
|
--duration/-d HOURS - Specifies the length of the search interval
|
|
in hours. (default: 24 hours)
|
|
|
|
Any two time options completely specify the time interval. If only
|
|
one option is provided, these defaults are used:
|
|
--until - Use the given end time with the default duration.
|
|
--since - Use the given start time with the default end time.
|
|
--duration - Use the given duration with the default end time.
|
|
|
|
If no time options are given, use the default end time and duration.
|
|
|
|
DATE/TIME values are of the form '2014-11-06 17:21:34'.
|
|
|
|
DUT Selection
|
|
~~~~~~~~~~~~~
|
|
By default, information is reported for DUTs named as command-line
|
|
arguments. Options are also available for selecting groups of
|
|
hosts:
|
|
--board/-b BOARD - Only include hosts with the given board.
|
|
--pool/-p POOL - Only include hosts in the given pool. The user
|
|
might be interested in the following pools: bvt, cq,
|
|
continuous, cts, or suites.
|
|
|
|
|
|
The selected hosts may also be filtered based on status:
|
|
-w/--working - Only include hosts in a working state.
|
|
-n/--broken - Only include hosts in a non-working state. Hosts
|
|
with no job history are considered non-working.
|
|
|
|
Output Formats
|
|
~~~~~~~~~~~~~~
|
|
There are four available output formats:
|
|
* A simple list of host names.
|
|
* A status summary showing one line per host.
|
|
* A detailed job history for all selected DUTs, sorted by
|
|
time of execution.
|
|
* A job history for all selected DUTs showing only the history
|
|
surrounding the DUT's last change from working to broken,
|
|
or vice versa.
|
|
|
|
The default format depends on whether hosts are filtered by
|
|
status:
|
|
* With the --working or --broken options, the list of host names
|
|
is the default format.
|
|
* Without those options, the default format is the one-line status
|
|
summary.
|
|
|
|
These options override the default formats:
|
|
-o/--oneline - Use the one-line summary with the --working or
|
|
--broken options.
|
|
-f/--full_history - Print detailed per-host job history.
|
|
-g/--diagnosis - Print the job history surrounding a status
|
|
change.
|
|
|
|
Examples
|
|
~~~~~~~~
|
|
$ dut_status chromeos2-row4-rack2-host12
|
|
hostname S last checked URL
|
|
chromeos2-row4-rack2-host12 NO 2014-11-06 15:25:29 http://...
|
|
|
|
'NO' means the DUT is broken. That diagnosis is based on a job that
|
|
failed: 'last checked' is the time of the failed job, and the URL
|
|
points to the job's logs.
|
|
|
|
$ dut_status.py -u '2014-11-06 15:30:00' -d 1 -f chromeos2-row4-rack2-host12
|
|
chromeos2-row4-rack2-host12
|
|
2014-11-06 15:25:29 NO http://...
|
|
2014-11-06 14:44:07 -- http://...
|
|
2014-11-06 14:42:56 OK http://...
|
|
|
|
The times are the start times of the jobs; the URL points to the
|
|
job's logs. The status indicates the working or broken status after
|
|
the job:
|
|
'NO' Indicates that the DUT was believed broken after the job.
|
|
'OK' Indicates that the DUT was believed working after the job.
|
|
'--' Indicates that the job probably didn't change the DUT's
|
|
status.
|
|
Typically, logs of the actual failure will be found at the last job
|
|
to report 'OK', or the first job to report '--'.
|
|
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
|
|
import common
|
|
from autotest_lib.client.common_lib import time_utils
|
|
from autotest_lib.server import constants
|
|
from autotest_lib.server import frontend
|
|
from autotest_lib.server.lib import status_history
|
|
from autotest_lib.utils import labellib
|
|
|
|
# The fully qualified name makes for lines that are too long, so
|
|
# shorten it locally.
|
|
HostJobHistory = status_history.HostJobHistory
|
|
|
|
# _DIAGNOSIS_IDS -
|
|
# Dictionary to map the known diagnosis codes to string values.
|
|
|
|
_DIAGNOSIS_IDS = {
|
|
status_history.UNUSED: '??',
|
|
status_history.UNKNOWN: '--',
|
|
status_history.WORKING: 'OK',
|
|
status_history.BROKEN: 'NO'
|
|
}
|
|
|
|
|
|
# Default time interval for the --duration option when a value isn't
|
|
# specified on the command line.
|
|
_DEFAULT_DURATION = 24
|
|
|
|
|
|
def _include_status(status, arguments):
|
|
"""Determine whether the given status should be filtered.
|
|
|
|
Checks the given `status` against the command line options in
|
|
`arguments`. Return whether a host with that status should be
|
|
printed based on the options.
|
|
|
|
@param status Status of a host to be printed or skipped.
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
|
|
@return Returns `True` if the command-line options call for
|
|
printing hosts with the status, or `False` otherwise.
|
|
|
|
"""
|
|
if status == status_history.WORKING:
|
|
return arguments.working
|
|
else:
|
|
return arguments.broken
|
|
|
|
|
|
def _print_host_summaries(history_list, arguments):
|
|
"""Print one-line summaries of host history.
|
|
|
|
This function handles the output format of the --oneline option.
|
|
|
|
@param history_list A list of HostHistory objects to be printed.
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
|
|
"""
|
|
fmt = '%-30s %-2s %-19s %s'
|
|
print(fmt % ('hostname', 'S', 'last checked', 'URL'))
|
|
for history in history_list:
|
|
status, event = history.last_diagnosis()
|
|
if not _include_status(status, arguments):
|
|
continue
|
|
datestr = '---'
|
|
url = '---'
|
|
if event is not None:
|
|
datestr = time_utils.epoch_time_to_date_string(
|
|
event.start_time)
|
|
url = event.job_url
|
|
|
|
print(fmt % (history.hostname,
|
|
_DIAGNOSIS_IDS[status],
|
|
datestr,
|
|
url))
|
|
|
|
|
|
def _print_event_summary(event):
|
|
"""Print a one-line summary of a job or special task."""
|
|
start_time = time_utils.epoch_time_to_date_string(
|
|
event.start_time)
|
|
print(' %s %s %s' % (
|
|
start_time,
|
|
_DIAGNOSIS_IDS[event.diagnosis],
|
|
event.job_url))
|
|
|
|
|
|
def _print_hosts(history_list, arguments):
|
|
"""Print hosts, optionally with a job history.
|
|
|
|
This function handles both the default format for --working
|
|
and --broken options, as well as the output for the
|
|
--full_history and --diagnosis options. The `arguments`
|
|
parameter determines the format to use.
|
|
|
|
@param history_list A list of HostHistory objects to be printed.
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
|
|
"""
|
|
for history in history_list:
|
|
status, _ = history.last_diagnosis()
|
|
if not _include_status(status, arguments):
|
|
continue
|
|
print(history.hostname)
|
|
if arguments.full_history:
|
|
for event in history:
|
|
_print_event_summary(event)
|
|
elif arguments.diagnosis:
|
|
for event in history.diagnosis_interval():
|
|
_print_event_summary(event)
|
|
|
|
|
|
def _validate_time_range(arguments):
|
|
"""Validate the time range requested on the command line.
|
|
|
|
Enforces the rules for the --until, --since, and --duration
|
|
options are followed, and calculates defaults:
|
|
* It isn't allowed to supply all three options.
|
|
* If only two options are supplied, they completely determine
|
|
the time interval.
|
|
* If only one option is supplied, or no options, then apply
|
|
specified defaults to the arguments object.
|
|
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
|
|
"""
|
|
if (arguments.duration is not None and
|
|
arguments.since is not None and arguments.until is not None):
|
|
print('FATAL: Can specify at most two of '
|
|
'--since, --until, and --duration',
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
if (arguments.until is None and (arguments.since is None or
|
|
arguments.duration is None)):
|
|
arguments.until = int(time.time())
|
|
if arguments.since is None:
|
|
if arguments.duration is None:
|
|
arguments.duration = _DEFAULT_DURATION
|
|
arguments.since = (arguments.until -
|
|
arguments.duration * 60 * 60)
|
|
elif arguments.until is None:
|
|
arguments.until = (arguments.since +
|
|
arguments.duration * 60 * 60)
|
|
|
|
|
|
def _get_host_histories(afe, arguments):
|
|
"""Return HostJobHistory objects for the requested hosts.
|
|
|
|
Checks that individual hosts specified on the command line are
|
|
valid. Invalid hosts generate a warning message, and are
|
|
omitted from futher processing.
|
|
|
|
The return value is a list of HostJobHistory objects for the
|
|
valid requested hostnames, using the time range supplied on the
|
|
command line.
|
|
|
|
@param afe Autotest frontend
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
@return List of HostJobHistory objects for the hosts requested
|
|
on the command line.
|
|
|
|
"""
|
|
histories = []
|
|
saw_error = False
|
|
for hostname in arguments.hostnames:
|
|
try:
|
|
h = HostJobHistory.get_host_history(
|
|
afe, hostname, arguments.since, arguments.until)
|
|
histories.append(h)
|
|
except:
|
|
print('WARNING: Ignoring unknown host %s' %
|
|
hostname, file=sys.stderr)
|
|
saw_error = True
|
|
if saw_error:
|
|
# Create separation from the output that follows
|
|
print(file=sys.stderr)
|
|
return histories
|
|
|
|
|
|
def _validate_host_list(afe, arguments):
|
|
"""Validate the user-specified list of hosts.
|
|
|
|
Hosts may be specified implicitly with --board or --pool, or
|
|
explictly as command line arguments. This enforces these
|
|
rules:
|
|
* If --board or --pool, or both are specified, individual
|
|
hosts may not be specified.
|
|
* However specified, there must be at least one host.
|
|
|
|
The return value is a list of HostJobHistory objects for the
|
|
requested hosts, using the time range supplied on the command
|
|
line.
|
|
|
|
@param afe Autotest frontend
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
@return List of HostJobHistory objects for the hosts requested
|
|
on the command line.
|
|
|
|
"""
|
|
if arguments.board or arguments.pool or arguments.model:
|
|
if arguments.hostnames:
|
|
print('FATAL: Hostname arguments provided '
|
|
'with --board or --pool', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
labels = labellib.LabelsMapping()
|
|
labels['board'] = arguments.board
|
|
labels['pool'] = arguments.pool
|
|
labels['model'] = arguments.model
|
|
histories = HostJobHistory.get_multiple_histories(
|
|
afe, arguments.since, arguments.until, labels.getlabels())
|
|
else:
|
|
histories = _get_host_histories(afe, arguments)
|
|
if not histories:
|
|
print('FATAL: no valid hosts found', file=sys.stderr)
|
|
sys.exit(1)
|
|
return histories
|
|
|
|
|
|
def _validate_format_options(arguments):
|
|
"""Check the options for what output format to use.
|
|
|
|
Enforce these rules:
|
|
* If neither --broken nor --working was used, then --oneline
|
|
becomes the selected format.
|
|
* If neither --broken nor --working was used, included both
|
|
working and broken DUTs.
|
|
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
|
|
"""
|
|
if (not arguments.oneline and not arguments.diagnosis and
|
|
not arguments.full_history):
|
|
arguments.oneline = (not arguments.working and
|
|
not arguments.broken)
|
|
if not arguments.working and not arguments.broken:
|
|
arguments.working = True
|
|
arguments.broken = True
|
|
|
|
|
|
def _validate_command(afe, arguments):
|
|
"""Check that the command's arguments are valid.
|
|
|
|
This performs command line checking to enforce command line
|
|
rules that ArgumentParser can't handle. Additionally, this
|
|
handles calculation of default arguments/options when a simple
|
|
constant default won't do.
|
|
|
|
Areas checked:
|
|
* Check that a valid time range was provided, supplying
|
|
defaults as necessary.
|
|
* Identify invalid host names.
|
|
|
|
@param afe Autotest frontend
|
|
@param arguments Parsed arguments object as returned by
|
|
ArgumentParser.parse_args().
|
|
@return List of HostJobHistory objects for the hosts requested
|
|
on the command line.
|
|
|
|
"""
|
|
_validate_time_range(arguments)
|
|
_validate_format_options(arguments)
|
|
return _validate_host_list(afe, arguments)
|
|
|
|
|
|
def _parse_command(argv):
|
|
"""Parse the command line arguments.
|
|
|
|
Create an argument parser for this command's syntax, parse the
|
|
command line, and return the result of the ArgumentParser
|
|
parse_args() method.
|
|
|
|
@param argv Standard command line argument vector; argv[0] is
|
|
assumed to be the command name.
|
|
@return Result returned by ArgumentParser.parse_args().
|
|
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
prog=argv[0],
|
|
description='Report DUT status and execution history',
|
|
epilog='You can specify one or two of --since, --until, '
|
|
'and --duration, but not all three.')
|
|
parser.add_argument('-s', '--since', type=status_history.parse_time,
|
|
metavar='DATE/TIME',
|
|
help=('Starting time for history display. '
|
|
'Format: "YYYY-MM-DD HH:MM:SS"'))
|
|
parser.add_argument('-u', '--until', type=status_history.parse_time,
|
|
metavar='DATE/TIME',
|
|
help=('Ending time for history display. '
|
|
'Format: "YYYY-MM-DD HH:MM:SS" '
|
|
'Default: now'))
|
|
parser.add_argument('-d', '--duration', type=int,
|
|
metavar='HOURS',
|
|
help='Number of hours of history to display'
|
|
' (default: %d)' % _DEFAULT_DURATION)
|
|
|
|
format_group = parser.add_mutually_exclusive_group()
|
|
format_group.add_argument('-f', '--full_history', action='store_true',
|
|
help='Display host history from most '
|
|
'to least recent for each DUT')
|
|
format_group.add_argument('-g', '--diagnosis', action='store_true',
|
|
help='Display host history for the '
|
|
'most recent DUT status change')
|
|
format_group.add_argument('-o', '--oneline', action='store_true',
|
|
help='Display host status summary')
|
|
|
|
parser.add_argument('-w', '--working', action='store_true',
|
|
help='List working devices by name only')
|
|
parser.add_argument('-n', '--broken', action='store_true',
|
|
help='List non-working devices by name only')
|
|
|
|
parser.add_argument('-b', '--board',
|
|
help='Display history for all DUTs '
|
|
'of the given board')
|
|
parser.add_argument('-m', '--model',
|
|
help='Display history for all DUTs of the given model.')
|
|
parser.add_argument('-p', '--pool',
|
|
help='Display history for all DUTs '
|
|
'in the given pool. You might '
|
|
'be interested in the following pools: '
|
|
+ ', '.join(constants.Pools.MANAGED_POOLS[:-1])
|
|
+', or '+ constants.Pools.MANAGED_POOLS[-1] +'.')
|
|
parser.add_argument('hostnames',
|
|
nargs='*',
|
|
help='Host names of DUTs to report on')
|
|
parser.add_argument('--web',
|
|
help='Master autotest frontend hostname. If no value '
|
|
'is given, the one in global config will be used.',
|
|
default=None)
|
|
arguments = parser.parse_args(argv[1:])
|
|
return arguments
|
|
|
|
|
|
def main(argv):
|
|
"""Standard main() for command line processing.
|
|
|
|
@param argv Command line arguments (normally sys.argv).
|
|
|
|
"""
|
|
arguments = _parse_command(argv)
|
|
afe = frontend.AFE(server=arguments.web)
|
|
history_list = _validate_command(afe, arguments)
|
|
if arguments.oneline:
|
|
_print_host_summaries(history_list, arguments)
|
|
else:
|
|
_print_hosts(history_list, arguments)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|