1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# This file contains utility functions for host_history.
6
7import collections
8import copy
9import multiprocessing.pool
10from itertools import groupby
11
12import common
13from autotest_lib.client.common_lib import time_utils
14from autotest_lib.client.common_lib.cros.graphite import autotest_es
15from autotest_lib.frontend import setup_django_environment
16from autotest_lib.frontend.afe import models
17from autotest_lib.site_utils import host_label_utils
18from autotest_lib.site_utils import job_history
19
20
21_HOST_HISTORY_TYPE = 'host_history'
22_LOCK_HISTORY_TYPE = 'lock_history'
23
24# The maximum number of days that the script will lookup for history.
25_MAX_DAYS_FOR_HISTORY = 90
26
27class NoHostFoundException(Exception):
28    """Exception raised when no host is found to search for history.
29    """
30
31
32def get_matched_hosts(board, pool):
33    """Get duts with matching board and pool labels from metaDB.
34
35    @param board: board of DUT, set to None if board doesn't need to match.
36    @param pool: pool of DUT, set to None if pool doesn't need to match.
37    @return: A list of duts that match the specified board and pool.
38    """
39    labels = []
40    if pool:
41        labels.append('pool:%s' % pool)
42    if board:
43        labels.append('board:%s' % board)
44    host_labels = host_label_utils.get_host_labels(labels=labels)
45    return host_labels.keys()
46
47
48def prepopulate_dict(keys, value, extras=None):
49    """Creates a dictionary with val=value for each key.
50
51    @param keys: list of keys
52    @param value: the value of each entry in the dict.
53    @param extras: list of additional keys
54    @returns: dictionary
55    """
56    result = collections.OrderedDict()
57    extra_keys = tuple(extras if extras else [])
58    for key in keys + extra_keys:
59        result[key] = value
60    return result
61
62
63def lock_history_to_intervals(initial_lock_val, t_start, t_end, lock_history):
64    """Converts lock history into a list of intervals of locked times.
65
66    @param initial_lock_val: Initial value of the lock (False or True)
67    @param t_start: beginning of the time period we are interested in.
68    @param t_end: end of the time period we are interested in.
69    @param lock_history: Result of querying es for locks (dict)
70           This dictionary should contain keys 'locked' and 'time_recorded'
71    @returns: Returns a list of tuples where the elements of each tuples
72           represent beginning and end of intervals of locked, respectively.
73    """
74    locked_intervals = []
75    t_prev = t_start
76    state_prev = initial_lock_val
77    for entry in lock_history.hits:
78        t_curr = entry['time_recorded']
79
80        #If it is locked, then we put into locked_intervals
81        if state_prev:
82            locked_intervals.append((t_prev, t_curr))
83
84        # update vars
85        t_prev = t_curr
86        state_prev = entry['locked']
87    if state_prev:
88        locked_intervals.append((t_prev, t_end))
89    return locked_intervals
90
91
92def find_most_recent_entry_before(t, type_str, hostname, fields):
93    """Returns the fields of the most recent entry before t.
94
95    @param t: time we are interested in.
96    @param type_str: _type in esdb, such as 'host_history' (string)
97    @param hostname: hostname of DUT (string)
98    @param fields: list of fields we are interested in
99    @returns: time, field_value of the latest entry.
100    """
101    # History older than 90 days are ignored. This helps the ES query faster.
102    t_epoch = time_utils.to_epoch_time(t)
103    result = autotest_es.query(
104            fields_returned=fields,
105            equality_constraints=[('_type', type_str),
106                                  ('hostname', hostname)],
107            range_constraints=[('time_recorded',
108                               t_epoch-3600*24*_MAX_DAYS_FOR_HISTORY, t_epoch)],
109            size=1,
110            sort_specs=[{'time_recorded': 'desc'}])
111    if result.total > 0:
112        return result.hits[0]
113    return {}
114
115
116def get_host_history_intervals(input):
117    """Gets stats for a host.
118
119    This method uses intervals found in metaDB to build a full history of the
120    host. The intervals argument contains a list of metadata from querying ES
121    for records between t_start and t_end. To get the status from t_start to
122    the first record logged in ES, we need to look back to the last record
123    logged in ES before t_start.
124
125    @param input: A dictionary of input args, which including following args:
126            t_start: beginning of time period we are interested in.
127            t_end: end of time period we are interested in.
128            hostname: hostname for the host we are interested in (string)
129            intervals: intervals from ES query.
130    @returns: dictionary, num_entries_found
131        dictionary of status: time spent in that status
132        num_entries_found: number of host history entries
133                           found in [t_start, t_end]
134
135    """
136    t_start = input['t_start']
137    t_end = input['t_end']
138    hostname = input['hostname']
139    intervals = input['intervals']
140    lock_history_recent = find_most_recent_entry_before(
141            t=t_start, type_str=_LOCK_HISTORY_TYPE, hostname=hostname,
142            fields=['time_recorded', 'locked'])
143    # I use [0] and [None] because lock_history_recent's type is list.
144    t_lock = lock_history_recent.get('time_recorded', None)
145    t_lock_val = lock_history_recent.get('locked', None)
146    t_metadata = find_most_recent_entry_before(
147            t=t_start, type_str=_HOST_HISTORY_TYPE, hostname=hostname,
148            fields=None)
149    t_host = t_metadata.pop('time_recorded', None)
150    t_host_stat = t_metadata.pop('status', None)
151    status_first = t_host_stat if t_host else 'Ready'
152    t = min([t for t in [t_lock, t_host, t_start] if t])
153
154    t_epoch = time_utils.to_epoch_time(t)
155    t_end_epoch = time_utils.to_epoch_time(t_end)
156    lock_history_entries = autotest_es.query(
157            fields_returned=['locked', 'time_recorded'],
158            equality_constraints=[('_type', _LOCK_HISTORY_TYPE),
159                                  ('hostname', hostname)],
160            range_constraints=[('time_recorded', t_epoch, t_end_epoch)],
161            sort_specs=[{'time_recorded': 'asc'}])
162
163    # Validate lock history. If an unlock event failed to be recorded in metadb,
164    # lock history will show the dut being locked while host still has status
165    # changed over the time. This check tries to remove the lock event in lock
166    # history if:
167    # 1. There is only one entry in lock_history_entries (it's a good enough
168    #    assumption to avoid the code being over complicated.
169    # 2. The host status has changes after the lock history starts as locked.
170    if (len(lock_history_entries.hits) == 1 and t_lock_val and
171        len(intervals) >1):
172        locked_intervals = None
173        print ('Lock history of dut %s is ignored, the dut may have missing '
174               'data in lock history in metadb. Try to lock and unlock the dut '
175               'in AFE will force the lock history to be updated in metadb.'
176               % hostname)
177    else:
178        locked_intervals = lock_history_to_intervals(t_lock_val, t, t_end,
179                                                     lock_history_entries)
180    num_entries_found = len(intervals)
181    t_prev = t_start
182    status_prev = status_first
183    metadata_prev = t_metadata
184    intervals_of_statuses = collections.OrderedDict()
185
186    for entry in intervals:
187        metadata = entry.copy()
188        t_curr = metadata.pop('time_recorded')
189        status_curr = metadata.pop('status')
190        intervals_of_statuses.update(calculate_status_times(
191                t_prev, t_curr, status_prev, metadata_prev, locked_intervals))
192        # Update vars
193        t_prev = t_curr
194        status_prev = status_curr
195        metadata_prev = metadata
196
197    # Do final as well.
198    intervals_of_statuses.update(calculate_status_times(
199            t_prev, t_end, status_prev, metadata_prev, locked_intervals))
200    return hostname, intervals_of_statuses, num_entries_found
201
202
203def calculate_total_times(intervals_of_statuses):
204    """Calculates total times in each status.
205
206    @param intervals_of_statuses: ordereddict where key=(ti, tf) and val=status
207    @returns: dictionary where key=status value=time spent in that status
208    """
209    total_times = prepopulate_dict(models.Host.Status.names, 0.0,
210                                   extras=['Locked'])
211    for key, status_info in intervals_of_statuses.iteritems():
212        ti, tf = key
213        total_times[status_info['status']] += tf - ti
214    return total_times
215
216
217def aggregate_hosts(intervals_of_statuses_list):
218    """Aggregates history of multiple hosts
219
220    @param intervals_of_statuses_list: A list of dictionaries where keys
221        are tuple (ti, tf), and value is the status along with other metadata.
222    @returns: A dictionary where keys are strings, e.g. 'status' and
223              value is total time spent in that status among all hosts.
224    """
225    stats_all = prepopulate_dict(models.Host.Status.names, 0.0,
226                                 extras=['Locked'])
227    num_hosts = len(intervals_of_statuses_list)
228    for intervals_of_statuses in intervals_of_statuses_list:
229        total_times = calculate_total_times(intervals_of_statuses)
230        for status, delta in total_times.iteritems():
231            stats_all[status] += delta
232    return stats_all, num_hosts
233
234
235def get_stats_string_aggregate(labels, t_start, t_end, aggregated_stats,
236                               num_hosts):
237    """Returns string reporting overall host history for a group of hosts.
238
239    @param labels: A list of labels useful for describing the group
240                   of hosts these overall stats represent.
241    @param t_start: beginning of time period we are interested in.
242    @param t_end: end of time period we are interested in.
243    @param aggregated_stats: A dictionary where keys are string, e.g. 'status'
244        value is total time spent in that status among all hosts.
245    @returns: string representing the aggregate stats report.
246    """
247    result = 'Overall stats for hosts: %s \n' % (', '.join(labels))
248    result += ' %s - %s \n' % (time_utils.epoch_time_to_date_string(t_start),
249                               time_utils.epoch_time_to_date_string(t_end))
250    result += ' Number of total hosts: %s \n' % (num_hosts)
251    # This is multiplied by time_spent to get percentage_spent
252    multiplication_factor = 100.0 / ((t_end - t_start) * num_hosts)
253    for status, time_spent in aggregated_stats.iteritems():
254        # Normalize by the total time we are interested in among ALL hosts.
255        spaces = ' ' * (15 - len(status))
256        percent_spent = multiplication_factor * time_spent
257        result += '    %s: %s %.2f %%\n' % (status, spaces, percent_spent)
258    result += '- -- --- ---- ----- ---- --- -- -\n'
259    return result
260
261
262def get_overall_report(label, t_start, t_end, intervals_of_statuses_list):
263    """Returns string reporting overall host history for a group of hosts.
264
265    @param label: A string that can be useful for showing what type group
266        of hosts these overall stats represent.
267    @param t_start: beginning of time period we are interested in.
268    @param t_end: end of time period we are interested in.
269    @param intervals_of_statuses_list: A list of dictionaries where keys
270        are tuple (ti, tf), and value is the status along with other metadata,
271        e.g., task_id, task_name, job_id etc.
272    """
273    stats_all, num_hosts = aggregate_hosts(
274            intervals_of_statuses_list)
275    return get_stats_string_aggregate(
276            label, t_start, t_end, stats_all, num_hosts)
277
278
279def get_intervals_for_host(t_start, t_end, hostname):
280    """Gets intervals for the given.
281
282    Query metaDB to return all intervals between given start and end time.
283    Note that intervals found in metaDB may miss the history from t_start to
284    the first interval found.
285
286    @param t_start: beginning of time period we are interested in.
287    @param t_end: end of time period we are interested in.
288    @param hosts: A list of hostnames to look for history.
289    @param board: Name of the board to look for history. Default is None.
290    @param pool: Name of the pool to look for history. Default is None.
291    @returns: A dictionary of hostname: intervals.
292    """
293    t_start_epoch = time_utils.to_epoch_time(t_start)
294    t_end_epoch = time_utils.to_epoch_time(t_end)
295    host_history_entries = autotest_es.query(
296                fields_returned=None,
297                equality_constraints=[('_type', _HOST_HISTORY_TYPE),
298                                      ('hostname', hostname)],
299                range_constraints=[('time_recorded', t_start_epoch,
300                                    t_end_epoch)],
301                sort_specs=[{'time_recorded': 'asc'}])
302    return host_history_entries.hits
303
304
305def get_intervals_for_hosts(t_start, t_end, hosts=None, board=None, pool=None):
306    """Gets intervals for given hosts or board/pool.
307
308    Query metaDB to return all intervals between given start and end time.
309    If a list of hosts is provided, the board and pool constraints are ignored.
310    If hosts is set to None, and board or pool is set, this method will attempt
311    to search host history with labels for all hosts, to help the search perform
312    faster.
313    If hosts, board and pool are all set to None, return intervals for all
314    hosts.
315    Note that intervals found in metaDB may miss the history from t_start to
316    the first interval found.
317
318    @param t_start: beginning of time period we are interested in.
319    @param t_end: end of time period we are interested in.
320    @param hosts: A list of hostnames to look for history.
321    @param board: Name of the board to look for history. Default is None.
322    @param pool: Name of the pool to look for history. Default is None.
323    @returns: A dictionary of hostname: intervals.
324    """
325    hosts_intervals = {}
326    if hosts:
327        for host in hosts:
328            hosts_intervals[host] = get_intervals_for_host(t_start, t_end, host)
329    else:
330        hosts = get_matched_hosts(board, pool)
331        if not hosts:
332            raise NoHostFoundException('No host is found for board:%s, pool:%s.'
333                                       % (board, pool))
334        equality_constraints=[('_type', _HOST_HISTORY_TYPE),]
335        if board:
336            equality_constraints.append(('labels', 'board:'+board))
337        if pool:
338            equality_constraints.append(('labels', 'pool:'+pool))
339        t_start_epoch = time_utils.to_epoch_time(t_start)
340        t_end_epoch = time_utils.to_epoch_time(t_end)
341        results =  autotest_es.query(
342                equality_constraints=equality_constraints,
343                range_constraints=[('time_recorded', t_start_epoch,
344                                    t_end_epoch)],
345                sort_specs=[{'hostname': 'asc'}])
346        results_group_by_host = {}
347        for hostname,intervals_for_host in groupby(results.hits,
348                                                   lambda h: h['hostname']):
349            results_group_by_host[hostname] = intervals_for_host
350        for host in hosts:
351            intervals = results_group_by_host.get(host, None)
352            # In case the host's board or pool label was modified after
353            # the last status change event was reported, we need to run a
354            # separate query to get its history. That way the host's
355            # history won't be shown as blank.
356            if not intervals:
357                intervals = get_intervals_for_host(t_start, t_end, host)
358            hosts_intervals[host] = intervals
359    return hosts_intervals
360
361
362def get_report(t_start, t_end, hosts=None, board=None, pool=None,
363                print_each_interval=False):
364    """Gets history for given hosts or board/pool
365
366    If a list of hosts is provided, the board and pool constraints are ignored.
367
368    @param t_start: beginning of time period we are interested in.
369    @param t_end: end of time period we are interested in.
370    @param hosts: A list of hostnames to look for history.
371    @param board: Name of the board to look for history. Default is None.
372    @param pool: Name of the pool to look for history. Default is None.
373    @param print_each_interval: True display all intervals, default is False.
374    @returns: stats report for this particular host. The report is a list of
375              tuples (stat_string, intervals, hostname), intervals is a sorted
376              dictionary.
377    """
378    if hosts:
379        board=None
380        pool=None
381
382    hosts_intervals = get_intervals_for_hosts(t_start, t_end, hosts, board,
383                                              pool)
384    history = {}
385    pool = multiprocessing.pool.ThreadPool(processes=16)
386    args = []
387    for hostname,intervals in hosts_intervals.items():
388        args.append({'t_start': t_start,
389                     't_end': t_end,
390                     'hostname': hostname,
391                     'intervals': intervals})
392    results = pool.imap_unordered(get_host_history_intervals, args)
393    for hostname, intervals, count in results:
394        history[hostname] = (intervals, count)
395    report = []
396    for hostname,intervals in history.items():
397        total_times = calculate_total_times(intervals[0])
398        stats = get_stats_string(
399                t_start, t_end, total_times, intervals[0], hostname,
400                intervals[1], print_each_interval)
401        report.append((stats, intervals[0], hostname))
402    return report
403
404
405def get_report_for_host(t_start, t_end, hostname, print_each_interval):
406    """Gets stats report for a host
407
408    @param t_start: beginning of time period we are interested in.
409    @param t_end: end of time period we are interested in.
410    @param hostname: hostname for the host we are interested in (string)
411    @param print_each_interval: True or False, whether we want to
412                                display all intervals
413    @returns: stats report for this particular host (string)
414    """
415    # Search for status change intervals during given time range.
416    intervals = get_intervals_for_host(t_start, t_end, hostname)
417    num_entries_found = len(intervals)
418    # Update the status change intervals with status before the first entry and
419    # host's lock history.
420    _, intervals_of_statuses = get_host_history_intervals(
421            {'t_start': t_start,
422             't_end': t_end,
423             'hostname': hostname,
424             'intervals': intervals})
425    total_times = calculate_total_times(intervals_of_statuses)
426    return (get_stats_string(
427                    t_start, t_end, total_times, intervals_of_statuses,
428                    hostname, num_entries_found, print_each_interval),
429                    intervals_of_statuses)
430
431
432def get_stats_string(t_start, t_end, total_times, intervals_of_statuses,
433                     hostname, num_entries_found, print_each_interval):
434    """Returns string reporting host_history for this host.
435    @param t_start: beginning of time period we are interested in.
436    @param t_end: end of time period we are interested in.
437    @param total_times: dictionary where key=status,
438                        value=(time spent in that status)
439    @param intervals_of_statuses: dictionary where keys is tuple (ti, tf),
440              and value is the status along with other metadata.
441    @param hostname: hostname for the host we are interested in (string)
442    @param num_entries_found: Number of entries found for the host in es
443    @param print_each_interval: boolean, whether to print each interval
444    """
445    delta = t_end - t_start
446    result = 'usage stats for host: %s \n' % (hostname)
447    result += ' %s - %s \n' % (time_utils.epoch_time_to_date_string(t_start),
448                               time_utils.epoch_time_to_date_string(t_end))
449    result += ' Num entries found in this interval: %s\n' % (num_entries_found)
450    for status, value in total_times.iteritems():
451        spaces = (15 - len(status)) * ' '
452        result += '    %s: %s %.2f %%\n' % (status, spaces, 100*value/delta)
453    result += '- -- --- ---- ----- ---- --- -- -\n'
454    if print_each_interval:
455        for interval, status_info in intervals_of_statuses.iteritems():
456            t0, t1 = interval
457            t0_string = time_utils.epoch_time_to_date_string(t0)
458            t1_string = time_utils.epoch_time_to_date_string(t1)
459            status = status_info['status']
460            delta = int(t1-t0)
461            id_info = status_info['metadata'].get(
462                    'task_id', status_info['metadata'].get('job_id', ''))
463            result += ('    %s  :  %s %-15s %-10s %ss\n' %
464                       (t0_string, t1_string, status, id_info, delta))
465    return result
466
467
468def calculate_status_times(t_start, t_end, int_status, metadata,
469                           locked_intervals):
470    """Returns a list of intervals along w/ statuses associated with them.
471
472    If the dut is in status Ready, i.e., int_status==Ready, the lock history
473    should be applied so that the time period when dut is locked is considered
474    as not available. Any other status is considered that dut is doing something
475    and being used. `Repair Failed` and Repairing are not checked with lock
476    status, since these two statuses indicate the dut is not available any way.
477
478    @param t_start: start time
479    @param t_end: end time
480    @param int_status: status of [t_start, t_end] if not locked
481    @param metadata: metadata of the status change, e.g., task_id, task_name.
482    @param locked_intervals: list of tuples denoting intervals of locked states
483    @returns: dictionary where key = (t_interval_start, t_interval_end),
484                               val = (status, metadata)
485              t_interval_start: beginning of interval for that status
486              t_interval_end: end of the interval for that status
487              status: string such as 'Repair Failed', 'Locked', etc.
488              metadata: A dictionary of metadata, e.g.,
489                              {'task_id':123, 'task_name':'Reset'}
490    """
491    statuses = collections.OrderedDict()
492
493    prev_interval_end = t_start
494
495    # TODO: Put allow more information here in info/locked status
496    status_info = {'status': int_status,
497                   'metadata': metadata}
498    locked_info = {'status': 'Locked',
499                   'metadata': {}}
500    if not locked_intervals:
501        statuses[(t_start, t_end)] = status_info
502        return statuses
503    for lock_start, lock_end in locked_intervals:
504        if prev_interval_end >= t_end:
505            break
506        if lock_start > t_end:
507            # optimization to break early
508            # case 0
509            # Timeline of status change: t_start t_end
510            # Timeline of lock action:                   lock_start lock_end
511            break
512        elif lock_end < prev_interval_end:
513            # case 1
514            #                      prev_interval_end    t_end
515            # lock_start lock_end
516            continue
517        elif lock_end <= t_end and lock_start >= prev_interval_end:
518            # case 2
519            # prev_interval_end                       t_end
520            #                    lock_start lock_end
521            # Lock happened in the middle, while the host stays in the same
522            # status, consider the lock has no effect on host history.
523            statuses[(prev_interval_end, lock_end)] = status_info
524            prev_interval_end = lock_end
525        elif lock_end > prev_interval_end and lock_start < prev_interval_end:
526            # case 3
527            #             prev_interval_end          t_end
528            # lock_start                    lock_end        (or lock_end)
529            # If the host status changed in the middle of being locked, consider
530            # the new status change as part of the host history.
531            statuses[(prev_interval_end, min(lock_end, t_end))] = locked_info
532            prev_interval_end = lock_end
533        elif lock_start < t_end and lock_end > t_end:
534            # case 4
535            # prev_interval_end             t_end
536            #                    lock_start        lock_end
537            # If the lock happens in the middle of host status change, consider
538            # the lock has no effect on the host history for that status.
539            statuses[(prev_interval_end, t_end)] = status_info
540            statuses[(lock_start, t_end)] = locked_info
541            prev_interval_end = t_end
542        # Otherwise we are in the case where lock_end < t_start OR
543        # lock_start > t_end, which means the lock doesn't apply.
544    if t_end > prev_interval_end:
545        # This is to avoid logging the same time
546        statuses[(prev_interval_end, t_end)] = status_info
547    return statuses
548
549
550def get_log_url(hostname, metadata):
551    """Compile a url to job's debug log from debug string.
552
553    @param hostname: Hostname of the dut.
554    @param metadata: A dictionary of other metadata, e.g.,
555                                     {'task_id':123, 'task_name':'Reset'}
556    @return: Url of the debug log for special task or job url for test job.
557    """
558    log_url = None
559    if 'task_id' in metadata and 'task_name' in metadata:
560        log_url = job_history.TASK_URL % {'hostname': hostname,
561                                          'task_id': metadata['task_id'],
562                                          'task_name': metadata['task_name']}
563    elif 'job_id' in metadata and 'owner' in metadata:
564        log_url = job_history.JOB_URL % {'hostname': hostname,
565                                         'job_id': metadata['job_id'],
566                                         'owner': metadata['owner']}
567
568    return log_url
569
570
571def build_history(hostname, status_intervals):
572    """Get host history information from given state intervals.
573
574    @param hostname: Hostname of the dut.
575    @param status_intervals: A ordered dictionary with
576                    key as (t_start, t_end) and value as (status, metadata)
577                    status = status of the host. e.g. 'Repair Failed'
578                    t_start is the beginning of the interval where the DUT's has
579                            that status
580                    t_end is the end of the interval where the DUT has that
581                            status
582                    metadata: A dictionary of other metadata, e.g.,
583                                        {'task_id':123, 'task_name':'Reset'}
584    @return: A list of host history, e.g.,
585             [{'status': 'Resetting'
586               'start_time': '2014-08-07 10:02:16',
587               'end_time': '2014-08-07 10:03:16',
588               'log_url': 'http://autotest/reset-546546/debug',
589               'task_id': 546546},
590              {'status': 'Running'
591               'start_time': '2014-08-07 10:03:18',
592               'end_time': '2014-08-07 10:13:00',
593               'log_url': 'http://autotest/afe/#tab_id=view_job&object_id=1683',
594               'job_id': 1683}
595             ]
596    """
597    history = []
598    for time_interval, status_info in status_intervals.items():
599        start_time = time_utils.epoch_time_to_date_string(time_interval[0])
600        end_time = time_utils.epoch_time_to_date_string(time_interval[1])
601        interval = {'status': status_info['status'],
602                    'start_time': start_time,
603                    'end_time': end_time}
604        interval['log_url'] = get_log_url(hostname, status_info['metadata'])
605        interval.update(status_info['metadata'])
606        history.append(interval)
607    return history
608
609
610def get_status_intervals(history_details):
611    """Get a list of status interval from history details.
612
613    This is a reverse method of above build_history. Caller gets the history
614    details from RPC get_host_history, and use this method to get the list of
615    status interval, which can be used to calculate stats from
616    host_history_utils.aggregate_hosts.
617
618    @param history_details: A dictionary of host history for each host, e.g.,
619            {'172.22.33.51': [{'status': 'Resetting'
620                               'start_time': '2014-08-07 10:02:16',
621                               'end_time': '2014-08-07 10:03:16',
622                               'log_url': 'http://autotest/reset-546546/debug',
623                               'task_id': 546546},]
624            }
625    @return: A list of dictionaries where keys are tuple (start_time, end_time),
626             and value is a dictionary containing at least key 'status'.
627    """
628    status_intervals = []
629    for host,history in history_details.iteritems():
630        intervals = collections.OrderedDict()
631        for interval in history:
632            start_time = time_utils.to_epoch_time(interval['start_time'])
633            end_time = time_utils.to_epoch_time(interval['end_time'])
634            metadata = copy.deepcopy(interval)
635            metadata['hostname'] = host
636            intervals[(start_time, end_time)] = {'status': interval['status'],
637                                                 'metadata': metadata}
638        status_intervals.append(intervals)
639    return status_intervals
640
641
642def get_machine_utilization_rate(stats):
643    """Get machine utilization rate from given stats.
644
645    @param stats: A dictionary with a status as key and value is the total
646                  number of seconds spent on the status.
647    @return: The percentage of time when dut is running test jobs.
648    """
649    not_utilized_status = ['Repairing', 'Repair Failed', 'Ready', 'Verifying']
650    excluded_status = ['Locked']
651    total_time = 0
652    total_time_not_utilized = 0.0
653    for status, interval in stats.iteritems():
654        if status in excluded_status:
655            continue
656        total_time += interval
657        if status in not_utilized_status:
658            total_time_not_utilized += interval
659    if total_time == 0:
660        # All duts are locked, assume MUR is 0%
661        return 0
662    else:
663        return 1 - total_time_not_utilized/total_time
664
665
666def get_machine_availability_rate(stats):
667    """Get machine availability rate from given stats.
668
669    @param stats: A dictionary with a status as key and value is the total
670                  number of seconds spent on the status.
671    @return: The percentage of time when dut is available to run jobs.
672    """
673    not_available_status = ['Repairing', 'Repair Failed', 'Verifying']
674    excluded_status = ['Locked']
675    total_time = 0
676    total_time_not_available = 0.0
677    for status, interval in stats.iteritems():
678        if status in excluded_status:
679            continue
680        total_time += interval
681        if status in not_available_status:
682            total_time_not_available += interval
683    if total_time == 0:
684        # All duts are locked, assume MAR is 0%
685        return 0
686    else:
687        return 1 - total_time_not_available/total_time
688