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