Home | History | Annotate | Download | only in site_utils
      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 
      7 import collections
      8 import copy
      9 import multiprocessing.pool
     10 from itertools import groupby
     11 
     12 import common
     13 from autotest_lib.client.common_lib import time_utils
     14 from autotest_lib.client.common_lib.cros.graphite import autotest_es
     15 from autotest_lib.frontend import setup_django_environment
     16 from autotest_lib.frontend.afe import models
     17 from autotest_lib.site_utils import host_label_utils
     18 from 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 
     27 class NoHostFoundException(Exception):
     28     """Exception raised when no host is found to search for history.
     29     """
     30 
     31 
     32 def 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 
     48 def 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 
     63 def 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 
     92 def 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 
    116 def 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 
    203 def 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 
    217 def 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 
    235 def 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 
    262 def 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 
    279 def 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 
    305 def 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 
    362 def 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 
    405 def 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 
    432 def 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 
    468 def 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 
    550 def 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 
    571 def 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 
    610 def 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 
    642 def 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 
    666 def 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