# Copyright (c) 2016 Clinton Knight
# All rights reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
"""
Performance metrics functions and cache for NetApp systems.
"""

import copy

from oslo_log import log as logging

from manila import exception
from manila.i18n import _
from manila.share.drivers.netapp.dataontap.client import api as netapp_api


LOG = logging.getLogger(__name__)
DEFAULT_UTILIZATION = 50


class PerformanceLibrary(object):

    def __init__(self, zapi_client):

        self.zapi_client = zapi_client
        self.performance_counters = {}
        self.pool_utilization = {}
        self._init_counter_info()

    def _init_counter_info(self):
        """Set a few counter names based on Data ONTAP version."""

        self.system_object_name = None
        self.avg_processor_busy_base_counter_name = None

        try:
            if self.zapi_client.features.SYSTEM_CONSTITUENT_METRICS:
                self.system_object_name = 'system:constituent'
                self.avg_processor_busy_base_counter_name = (
                    self._get_base_counter_name('system:constituent',
                                                'avg_processor_busy'))
            elif self.zapi_client.features.SYSTEM_METRICS:
                self.system_object_name = 'system'
                self.avg_processor_busy_base_counter_name = (
                    self._get_base_counter_name('system',
                                                'avg_processor_busy'))
        except netapp_api.NaApiError:
            if self.zapi_client.features.SYSTEM_CONSTITUENT_METRICS:
                self.avg_processor_busy_base_counter_name = 'cpu_elapsed_time'
            else:
                self.avg_processor_busy_base_counter_name = 'cpu_elapsed_time1'
            LOG.exception('Could not get performance base counter '
                          'name. Performance-based scheduler '
                          'functions may not be available.')

    def update_performance_cache(self, flexvol_pools, aggregate_pools):
        """Called periodically to update per-pool node utilization metrics."""

        # Nothing to do on older systems
        if not (self.zapi_client.features.SYSTEM_METRICS or
                self.zapi_client.features.SYSTEM_CONSTITUENT_METRICS):
            return

        # Get aggregates and nodes for all known pools
        aggr_names = self._get_aggregates_for_pools(flexvol_pools,
                                                    aggregate_pools)
        node_names, aggr_node_map = self._get_nodes_for_aggregates(aggr_names)

        # Update performance counter cache for each node
        node_utilization = {}
        for node_name in node_names:
            if node_name not in self.performance_counters:
                self.performance_counters[node_name] = []

            # Get new performance counters and save only the last 10
            counters = self._get_node_utilization_counters(node_name)
            if not counters:
                continue

            self.performance_counters[node_name].append(counters)
            self.performance_counters[node_name] = (
                self.performance_counters[node_name][-10:])

            # Update utilization for each node using newest & oldest sample
            counters = self.performance_counters[node_name]
            if len(counters) < 2:
                node_utilization[node_name] = DEFAULT_UTILIZATION
            else:
                node_utilization[node_name] = self._get_node_utilization(
                    counters[0], counters[-1], node_name)

        # Update pool utilization map atomically
        pool_utilization = {}
        all_pools = copy.deepcopy(flexvol_pools)
        all_pools.update(aggregate_pools)
        for pool_name, pool_info in all_pools.items():
            aggr_name = pool_info.get('netapp_aggregate', 'unknown')
            node_name = aggr_node_map.get(aggr_name)
            if node_name:
                pool_utilization[pool_name] = node_utilization.get(
                    node_name, DEFAULT_UTILIZATION)
            else:
                pool_utilization[pool_name] = DEFAULT_UTILIZATION

        self.pool_utilization = pool_utilization

    def get_node_utilization_for_pool(self, pool_name):
        """Get the node utilization for the specified pool, if available."""

        return self.pool_utilization.get(pool_name, DEFAULT_UTILIZATION)

    def update_for_failover(self, zapi_client, flexvol_pools, aggregate_pools):
        """Change API client after a whole-backend failover event."""

        self.zapi_client = zapi_client
        self.update_performance_cache(flexvol_pools, aggregate_pools)

    def _get_aggregates_for_pools(self, flexvol_pools, aggregate_pools):
        """Get the set of aggregates that contain the specified pools."""

        aggr_names = set()
        for pool_name, pool_info in aggregate_pools.items():
            aggr_names.add(pool_info.get('netapp_aggregate'))
        for pool_name, pool_info in flexvol_pools.items():
            aggr_names.add(pool_info.get('netapp_aggregate'))
        return list(aggr_names)

    def _get_nodes_for_aggregates(self, aggr_names):
        """Get the cluster nodes that own the specified aggregates."""

        node_names = set()
        aggr_node_map = {}

        for aggr_name in aggr_names:
            node_name = self.zapi_client.get_node_for_aggregate(aggr_name)
            if node_name:
                node_names.add(node_name)
                aggr_node_map[aggr_name] = node_name

        return list(node_names), aggr_node_map

    def _get_node_utilization(self, counters_t1, counters_t2, node_name):
        """Get node utilization from two sets of performance counters."""

        try:
            # Time spent in the single-threaded Kahuna domain
            kahuna_percent = self._get_kahuna_utilization(counters_t1,
                                                          counters_t2)

            # If Kahuna is using >60% of the CPU, the controller is fully busy
            if kahuna_percent > 60:
                return 100.0

            # Average CPU busyness across all processors
            avg_cpu_percent = 100.0 * self._get_average_cpu_utilization(
                counters_t1, counters_t2)

            # Total Consistency Point (CP) time
            total_cp_time_msec = self._get_total_consistency_point_time(
                counters_t1, counters_t2)

            # Time spent in CP Phase 2 (buffer flush)
            p2_flush_time_msec = self._get_consistency_point_p2_flush_time(
                counters_t1, counters_t2)

            # Wall-clock time between the two counter sets
            poll_time_msec = self._get_total_time(counters_t1,
                                                  counters_t2,
                                                  'total_cp_msecs')

            # If two polls happened in quick succession, use CPU utilization
            if total_cp_time_msec == 0 or poll_time_msec == 0:
                return max(min(100.0, avg_cpu_percent), 0)

            # Adjusted Consistency Point time
            adjusted_cp_time_msec = self._get_adjusted_consistency_point_time(
                total_cp_time_msec, p2_flush_time_msec)
            adjusted_cp_percent = (100.0 *
                                   adjusted_cp_time_msec / poll_time_msec)

            # Utilization is the greater of CPU busyness & CP time
            node_utilization = max(avg_cpu_percent, adjusted_cp_percent)
            return max(min(100.0, node_utilization), 0)

        except Exception:
            LOG.exception('Could not calculate node utilization for '
                          'node %s.', node_name)
            return DEFAULT_UTILIZATION

    def _get_kahuna_utilization(self, counters_t1, counters_t2):
        """Get time spent in the single-threaded Kahuna domain."""

        # Note(cknight): Because Kahuna is single-threaded, running only on
        # one CPU at a time, we can safely sum the Kahuna CPU usage
        # percentages across all processors in a node.
        return sum(self._get_performance_counter_average_multi_instance(
            counters_t1, counters_t2, 'domain_busy:kahuna',
            'processor_elapsed_time')) * 100.0

    def _get_average_cpu_utilization(self, counters_t1, counters_t2):
        """Get average CPU busyness across all processors."""

        return self._get_performance_counter_average(
            counters_t1, counters_t2, 'avg_processor_busy',
            self.avg_processor_busy_base_counter_name)