lin
2025-07-30 fcd736bf35fd93b563e9bbf594f2aa7b62028cc9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
#!/usr/bin/env python
# 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 '--'.
 
"""
 
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 >>sys.stderr, ('FATAL: Can specify at most two of '
                             '--since, --until, and --duration')
        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 >>sys.stderr, ('WARNING: Ignoring unknown host %s' %
                                  hostname)
            saw_error = True
    if saw_error:
        # Create separation from the output that follows
        print >>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 >>sys.stderr, ('FATAL: Hostname arguments provided '
                                 'with --board or --pool')
            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 >>sys.stderr, 'FATAL: no valid hosts found'
        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)