#!/usr/bin/env python

# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Script to check the history of stage calls made to devserver.
# Following are some sample use cases:
#
# 1. Find all stage request for autotest and image nyan_big-release/R38-6055.0.0
#    in the last 10 days across all devservers.
# ./devserver_history.py --image_filters nyan_big 38 6055.0.0 -l 240 \
#                        --artifact_filters autotest -v
# output:
# ==============================================================================
# 170.21.64.22
# ==============================================================================
# Number of calls:         1
# Number of unique images: 1
# 2014-08-23 12:45:00: nyan_big-release/R38-6055.0.0    autotest
# ==============================================================================
# 170.21.64.23
# ==============================================================================
# Number of calls:         2
# Number of unique images: 1
# 2014-08-23 12:45:00: nyan_big-release/R38-6055.0.0    autotest, test_suites
# 2014-08-23 12:55:00: nyan_big-release/R38-6055.0.0    autotest, test_suites
#
# 2. Find all duplicated stage request for the last 10 days.
# ./devserver_history.py -d -l 240
# output:
# Detecting artifacts staged in multiple devservers.
# ==============================================================================
# nyan_big-release/R38-6055.0.0
# ==============================================================================
# 170.21.64.22: 23  requests 2014-09-04 22:44:28 -- 2014-09-05 00:03:23
# 170.21.64.23: 6   requests 2014-09-04 22:48:58 -- 2014-09-04 22:49:42
#
# Count of images with duplicated stages on each devserver:
# 170.21.64.22   : 22
# 170.21.64.23   : 11


import argparse
import datetime
import logging
import operator
import re
import time
from itertools import groupby

import common
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib import time_utils
from autotest_lib.client.common_lib.cros.graphite import autotest_es


class devserver_call(object):
    """A container to store the information of devserver stage call.
    """

    def __init__(self, hit):
        """Retrieve information from a ES query hit.
        """
        self.devserver = hit['devserver']
        self.subname = hit['subname']
        self.artifacts = hit['artifacts'].split(' ')
        self.image = hit['image']
        self.value = hit['value']
        self.time_recorded = time_utils.epoch_time_to_date_string(
                hit['time_recorded'])


    def __str__(self):
        pairs = ['%-20s: %s' % (attr, getattr(self, attr)) for attr in dir(self)
                  if not attr.startswith('__') and
                  not callable(getattr(self, attr))]
        return '\n'.join(pairs)


def get_calls(time_start, time_end, artifact_filters=None,
              regex_constraints=None, devserver=None, size=1e7):
    """Gets all devserver calls from es db with the given constraints.

    @param time_start: Earliest time entry was recorded.
    @param time_end: Latest time entry was recorded.
    @param artifact_filters: A list of names to match artifacts.
    @param regex_constraints: A list of regex constraints for ES query.
    @param devserver: name of devserver to query for. If it's set to None,
                      return calls for all devservers. Default is set to None.
    @param size: Max number of entries to return, default to 1 million.

    @returns: Entries from esdb.
    """
    eqs = [('_type', 'devserver')]
    if devserver:
        eqs.append(('devserver', devserver))
    if artifact_filters:
        for artifact in artifact_filters:
            eqs.append(('artifacts', artifact))
    time_start_epoch = time_utils.to_epoch_time(time_start)
    time_end_epoch = time_utils.to_epoch_time(time_end)
    results = autotest_es.query(
            fields_returned=None,
            equality_constraints=eqs,
            range_constraints=[('time_recorded', time_start_epoch,
                                time_end_epoch)],
            size=size,
            sort_specs=[{'time_recorded': 'desc'}],
            regex_constraints=regex_constraints)
    devserver_calls = []
    for hit in results.hits:
        devserver_calls.append(devserver_call(hit))
    logging.info('Found %d calls.', len(devserver_calls))
    return devserver_calls


def print_call_details(calls, verbose):
    """Print details of each call to devserver to stage artifacts.

    @param calls: A list of devserver stage requests.
    @param verbose: Set to True to print out all devserver calls.
    """
    calls = sorted(calls, key=lambda c: c.devserver)
    for devserver,calls_for_devserver in groupby(calls, lambda c: c.devserver):
        calls_for_devserver = list(calls_for_devserver)
        print '='*80
        print devserver
        print '='*80
        print 'Number of calls:         %d' % len(calls_for_devserver)
        print ('Number of unique images: %d' %
               len(set([call.image for call in calls_for_devserver])))
        if verbose:
            for call in sorted(calls_for_devserver,
                               key=lambda c: c.time_recorded):
                print ('%s %s    %s' % (call.time_recorded, call.image,
                                         ', '.join(call.artifacts)))


def detect_duplicated_stage(calls):
    """Detect any artifact for same build was staged in multiple devservers.

    @param calls: A list of devserver stage requests.
    """
    print '\nDetecting artifacts staged in multiple devservers.'
    calls = sorted(calls, key=lambda c: c.image)
    # Count how many times a devserver staged duplicated artifacts. A number
    # significantly larger then others can indicate that the devserver failed
    # check_health too often and needs to be removed from production.
    duplicated_stage_count = {}
    for image,calls_for_image in groupby(calls, lambda c: c.image):
        calls_for_image = list(calls_for_image)
        devservers = set([call.devserver for call in calls_for_image])
        if len(devservers) > 1:
            print '='*80
            print image
            print '='*80
            calls_for_image = sorted(calls_for_image, key=lambda c: c.devserver)
            for devserver,calls_for_devserver in groupby(calls_for_image,
                                                         lambda c: c.devserver):
                timestamps = [c.time_recorded for c in calls_for_devserver]
                print ('%s: %-3d requests %s -- %s' %
                       (devserver, len(timestamps), min(timestamps),
                        max(timestamps)))
                duplicated_stage_count[devserver] = (
                        duplicated_stage_count.get(devserver, 0) + 1)
    print '\nCount of images with duplicated stages on each devserver:'
    counts = sorted(duplicated_stage_count.iteritems(),
                    key=operator.itemgetter(1), reverse=True)
    for k,v in counts:
        print '%-15s: %d' % (k, v)


def main():
    """main script. """
    t_now = time.time()
    t_now_minus_one_day = t_now - 3600 * 24
    parser = argparse.ArgumentParser()
    parser.add_argument('-l', type=float, dest='last',
                        help='last hours to search results across',
                        default=None)
    parser.add_argument('--start', type=str, dest='start',
                        help=('Enter start time as: yyyy-mm-dd hh-mm-ss,'
                              'defualts to 24h ago. This option is ignored when'
                              ' -l is used.'),
                        default=time_utils.epoch_time_to_date_string(
                                t_now_minus_one_day))
    parser.add_argument('--end', type=str, dest='end',
                        help=('Enter end time in as: yyyy-mm-dd hh-mm-ss,'
                              'defualts to current time. This option is ignored'
                              ' when -l is used.'),
                        default=time_utils.epoch_time_to_date_string(t_now))
    parser.add_argument('--devservers', nargs='+', dest='devservers',
                         help=('Enter space deliminated devservers. Default are'
                               ' all devservers specified in global config.'),
                         default=[])
    parser.add_argument('--artifact_filters', nargs='+',
                        dest='artifact_filters',
                        help=('Enter space deliminated filters on artifact '
                              'name. For example "autotest test_suites". The '
                              'filter does not support regex.'),
                        default=[])
    parser.add_argument('--image_filters', nargs='+', dest='image_filters',
                         help=('Enter space deliminated filters on image name. '
                               'For example "nyan 38 6566", search will use '
                               'regex to match each filter. Do not use filters '
                               'with mixed letter and number, e.g., R38.'),
                         default=[])
    parser.add_argument('-d', '--detect_duplicated_stage', action='store_true',
                        dest='detect_duplicated_stage',
                        help=('Set to True to detect if an artifacts for a same'
                              ' build was staged in multiple devservers. '
                              'Default is True.'),
                        default=False)
    parser.add_argument('-v', action='store_true', dest='verbose',
                        default=False,
                        help='-v to print out ALL entries.')
    options = parser.parse_args()
    if options.verbose:
        logging.getLogger().setLevel(logging.INFO)

    if options.last:
        end_time = datetime.datetime.now()
        start_time = end_time - datetime.timedelta(seconds=3600 * options.last)
    else:
        start_time = datetime.datetime.strptime(options.start,
                                                time_utils.TIME_FMT)
        end_time = datetime.datetime.strptime(options.end, time_utils.TIME_FMT)
    logging.info('Searching devserver calls from %s to %s', start_time,
                 end_time)

    devservers = options.devservers
    if not devservers:
        devserver_urls = global_config.global_config.get_config_value(
                'CROS', 'dev_server', type=list, default=[])
        devservers = []
        for url in devserver_urls:
            match = re.match('http://([^:]*):*\d*', url)
            devservers.append(match.groups(0)[0] if match else url)
    logging.info('Found devservers: %s', devservers)

    regex_constraints = []
    for filter in options.image_filters:
        regex_constraints.append(('image', '.*%s.*' % filter))
    calls = []
    for devserver in devservers:
        calls.extend(get_calls(start_time, end_time, options.artifact_filters,
                               regex_constraints, devserver=devserver))

    print_call_details(calls, options.verbose)

    if options.detect_duplicated_stage:
        detect_duplicated_stage(calls)


if __name__ == '__main__':
    main()