# Copyright (c) 2012 Intel
# Copyright (c) 2012 OpenStack Foundation
# Copyright (c) 2015 EMC Corporation
#
# 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.


from oslo_log import log as logging

from cinder.scheduler import filters
from cinder import utils


LOG = logging.getLogger(__name__)


class CapacityFilter(filters.BaseBackendFilter):
    """Capacity filters based on volume backend's capacity utilization."""

    def backend_passes(self, backend_state, filter_properties):
        """Return True if host has sufficient capacity."""

        volid = None
        # If the volume already exists on this host, don't fail it for
        # insufficient capacity (e.g., if we are retyping)
        if backend_state.backend_id == filter_properties.get('vol_exists_on'):
            return True

        spec = filter_properties.get('request_spec')
        if spec:
            volid = spec.get('volume_id')

        grouping = 'cluster' if backend_state.cluster_name else 'host'
        if filter_properties.get('new_size'):
            # If new_size is passed, we are allocating space to extend a volume
            requested_size = (int(filter_properties.get('new_size')) -
                              int(filter_properties.get('size')))
            LOG.debug('Checking if %(grouping)s %(grouping_name)s can extend '
                      'the volume %(id)s in %(size)s GB',
                      {'grouping': grouping,
                       'grouping_name': backend_state.backend_id, 'id': volid,
                       'size': requested_size})
        else:
            requested_size = filter_properties.get('size')
            LOG.debug('Checking if %(grouping)s %(grouping_name)s can create '
                      'a %(size)s GB volume (%(id)s)',
                      {'grouping': grouping,
                       'grouping_name': backend_state.backend_id, 'id': volid,
                       'size': requested_size})

        # requested_size is 0 means that it's a manage request.
        if requested_size == 0:
            return True

        if backend_state.free_capacity_gb is None:
            # Fail Safe
            LOG.error("Free capacity not set: "
                      "volume node info collection broken.")
            return False

        free_space = backend_state.free_capacity_gb
        total_space = backend_state.total_capacity_gb
        reserved = float(backend_state.reserved_percentage) / 100
        if free_space in ['infinite', 'unknown']:
            # NOTE(zhiteng) for those back-ends cannot report actual
            # available capacity, we assume it is able to serve the
            # request.  Even if it was not, the retry mechanism is
            # able to handle the failure by rescheduling
            return True
        elif total_space in ['infinite', 'unknown']:
            # If total_space is 'infinite' or 'unknown' and reserved
            # is 0, we assume the back-ends can serve the request.
            # If total_space is 'infinite' or 'unknown' and reserved
            # is not 0, we cannot calculate the reserved space.
            # float(total_space) will throw an exception. total*reserved
            # also won't work. So the back-ends cannot serve the request.
            if reserved == 0:
                return True
            LOG.debug("Cannot calculate GB of reserved space (%s%%) with "
                      "backend's reported total capacity '%s'",
                      backend_state.reserved_percentage, total_space)
            return False
        total = float(total_space)
        if total <= 0:
            LOG.warning("Insufficient free space for volume creation. "
                        "Total capacity is %(total).2f on %(grouping)s "
                        "%(grouping_name)s.",
                        {"total": total,
                         "grouping": grouping,
                         "grouping_name": backend_state.backend_id})
            return False

        # NOTE(xyang): If 'provisioning:type' is 'thick' in extra_specs,
        # we will not use max_over_subscription_ratio and
        # provisioned_capacity_gb to determine whether a volume can be
        # provisioned. Instead free capacity will be used to evaluate.
        thin = True
        vol_type = filter_properties.get('volume_type', {}) or {}
        provision_type = vol_type.get('extra_specs', {}).get(
            'provisioning:type')
        if provision_type == 'thick':
            thin = False

        thin_support = backend_state.thin_provisioning_support
        if thin_support:
            max_over_subscription_ratio = (
                backend_state.max_over_subscription_ratio
            )
        else:
            max_over_subscription_ratio = 1

        # NOTE(hemna): this takes into consideration all major factors
        # including reserved space, free_space (reported by driver),
        # and over subscription ratio.
        factors = utils.calculate_capacity_factors(
            total_space,
            free_space,
            backend_state.provisioned_capacity_gb,
            thin_support,
            max_over_subscription_ratio,
            backend_state.reserved_percentage,
            thin
        )
        virtual_free_space = factors["virtual_free_capacity"]
        LOG.debug("Storage Capacity factors %s", factors)

        msg_args = {"grouping_name": backend_state.backend_id,
                    "grouping": grouping,
                    "requested": requested_size,
                    "available": virtual_free_space}

        # Only evaluate using max_over_subscription_ratio if
        # thin_provisioning_support is True. Check if the ratio of
        # provisioned capacity over total capacity has exceeded over
        # subscription ratio.
        if (thin and backend_state.thin_provisioning_support and
                backend_state.max_over_subscription_ratio >= 1):
            provisioned_ratio = (
                (backend_state.provisioned_capacity_gb + requested_size) / (
                    factors["total_available_capacity"]
                )
            )
            LOG.debug("Checking provisioning for request of %s GB. "
                      "Backend: %s", requested_size, backend_state)
            if provisioned_ratio > backend_state.max_over_subscription_ratio:
                msg_args = {
                    "provisioned_ratio": provisioned_ratio,
                    "oversub_ratio": backend_state.max_over_subscription_ratio,
                    "grouping": grouping,
                    "grouping_name": backend_state.backend_id,
                }
                LOG.warning(
                    "Insufficient free space for thin provisioning. "
                    "The ratio of provisioned capacity over total capacity "
                    "%(provisioned_ratio).2f has exceeded the maximum over "
                    "subscription ratio %(oversub_ratio).2f on %(grouping)s "
                    "%(grouping_name)s.", msg_args)
                return False
            else:
                # Thin provisioning is enabled and projected over-subscription
                # ratio does not exceed max_over_subscription_ratio. The host
                # passes if virtual free capacity is enough to
                # accommodate the volume. Adjusted free virtual capacity is
                # the currently available free capacity (taking into account
                # of reserved space) which we can over-subscribe.
                msg_args["available"] = virtual_free_space
                res = virtual_free_space >= requested_size
                if not res:
                    LOG.warning("Insufficient free virtual space "
                                "(%(available)sGB) to accommodate thin "
                                "provisioned %(requested)sGB volume on "
                                "%(grouping)s %(grouping_name)s.", msg_args)
                else:
                    LOG.debug("Space information for volume creation "
                              "on %(grouping)s %(grouping_name)s "
                              "(requested / avail): "
                              "%(requested)s/%(available)s", msg_args)
                return res
        elif thin and backend_state.thin_provisioning_support:
            LOG.warning("Filtering out %(grouping)s %(grouping_name)s "
                        "with an invalid maximum over subscription ratio "
                        "of %(oversub_ratio).2f. The ratio should be a "
                        "minimum of 1.0.",
                        {"oversub_ratio":
                            backend_state.max_over_subscription_ratio,
                         "grouping": grouping,
                         "grouping_name": backend_state.backend_id})
            return False

        if virtual_free_space < requested_size:
            LOG.warning("Insufficient free space for volume creation "
                        "on %(grouping)s %(grouping_name)s (requested / "
                        "avail): %(requested)s/%(available)s",
                        msg_args)
            return False

        LOG.debug("Space information for volume creation "
                  "on %(grouping)s %(grouping_name)s (requested / avail): "
                  "%(requested)s/%(available)s", msg_args)

        return True
