# Copyright (C) 2012 - 2014 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.

"""
Handles all requests relating to consistency groups.
"""


import functools

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import timeutils

from cinder.common import constants
from cinder import db
from cinder.db import base
from cinder import exception
from cinder.i18n import _, _LE, _LW
from cinder import objects
from cinder.objects import fields as c_fields
import cinder.policy
from cinder import quota
from cinder.scheduler import rpcapi as scheduler_rpcapi
from cinder.volume import api as volume_api
from cinder.volume import rpcapi as volume_rpcapi
from cinder.volume import utils as vol_utils
from cinder.volume import volume_types


CONF = cfg.CONF

LOG = logging.getLogger(__name__)
CGQUOTAS = quota.CGQUOTAS
VALID_REMOVE_VOL_FROM_CG_STATUS = (
    'available',
    'in-use',
    'error',
    'error_deleting')
VALID_ADD_VOL_TO_CG_STATUS = (
    'available',
    'in-use')


def wrap_check_policy(func):
    """Check policy corresponding to the wrapped methods prior to execution.

    This decorator requires the first 3 args of the wrapped function
    to be (self, context, consistencygroup)
    """
    @functools.wraps(func)
    def wrapped(self, context, target_obj, *args, **kwargs):
        check_policy(context, func.__name__, target_obj)
        return func(self, context, target_obj, *args, **kwargs)

    return wrapped


def check_policy(context, action, target_obj=None):
    target = {
        'project_id': context.project_id,
        'user_id': context.user_id,
    }
    target_obj = target_obj.fields if target_obj else {}
    target.update(target_obj)
    _action = 'consistencygroup:%s' % action
    cinder.policy.enforce(context, _action, target)


class API(base.Base):
    """API for interacting with the volume manager for consistency groups."""

    def __init__(self, db_driver=None):
        self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
        self.volume_rpcapi = volume_rpcapi.VolumeAPI()
        self.availability_zone_names = ()
        self.volume_api = volume_api.API()

        super(API, self).__init__(db_driver)

    def _valid_availability_zone(self, availability_zone):
        if availability_zone in self.availability_zone_names:
            return True
        if CONF.storage_availability_zone == availability_zone:
            return True
        azs = self.volume_api.list_availability_zones()
        self.availability_zone_names = [az['name'] for az in azs]
        return availability_zone in self.availability_zone_names

    def _extract_availability_zone(self, availability_zone):
        if availability_zone is None:
            if CONF.default_availability_zone:
                availability_zone = CONF.default_availability_zone
            else:
                # For backwards compatibility use the storage_availability_zone
                availability_zone = CONF.storage_availability_zone

        valid = self._valid_availability_zone(availability_zone)
        if not valid:
            msg = _LW(
                "Availability zone '%s' is invalid") % (availability_zone)
            LOG.warning(msg)
            raise exception.InvalidInput(reason=msg)

        return availability_zone

    def create(self, context, name, description,
               cg_volume_types, availability_zone=None):
        check_policy(context, 'create')

        volume_type_list = None
        volume_type_list = cg_volume_types.split(',')

        req_volume_types = []
        # NOTE: Admin context is required to get extra_specs of volume_types.
        req_volume_types = (self.db.volume_types_get_by_name_or_id(
            context.elevated(), volume_type_list))

        req_volume_type_ids = ""
        for voltype in req_volume_types:
            req_volume_type_ids = (
                req_volume_type_ids + voltype.get('id') + ",")
        if len(req_volume_type_ids) == 0:
            req_volume_type_ids = None

        availability_zone = self._extract_availability_zone(availability_zone)
        kwargs = {'user_id': context.user_id,
                  'project_id': context.project_id,
                  'availability_zone': availability_zone,
                  'status': c_fields.ConsistencyGroupStatus.CREATING,
                  'name': name,
                  'description': description,
                  'volume_type_id': req_volume_type_ids}
        group = None
        try:
            group = objects.ConsistencyGroup(context=context, **kwargs)
            group.create()
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Error occurred when creating consistency group"
                              " %s."), name)

        request_spec_list = []
        filter_properties_list = []
        for req_volume_type in req_volume_types:
            request_spec = {'volume_type': req_volume_type.copy(),
                            'consistencygroup_id': group.id}
            filter_properties = {}
            request_spec_list.append(request_spec)
            filter_properties_list.append(filter_properties)

        # Update quota for consistencygroups
        self.update_quota(context, group, 1)

        self._cast_create_consistencygroup(context, group,
                                           request_spec_list,
                                           filter_properties_list)

        return group

    def create_from_src(self, context, name, description=None,
                        cgsnapshot_id=None, source_cgid=None):
        check_policy(context, 'create')

        kwargs = {
            'user_id': context.user_id,
            'project_id': context.project_id,
            'status': c_fields.ConsistencyGroupStatus.CREATING,
            'name': name,
            'description': description,
            'cgsnapshot_id': cgsnapshot_id,
            'source_cgid': source_cgid,
        }

        group = None
        try:
            group = objects.ConsistencyGroup(context=context, **kwargs)
            group.create(cg_snap_id=cgsnapshot_id, cg_id=source_cgid)
        except exception.ConsistencyGroupNotFound:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Source CG %(source_cg)s not found when "
                              "creating consistency group %(cg)s from "
                              "source."),
                          {'cg': name, 'source_cg': source_cgid})
        except exception.CgSnapshotNotFound:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("CG snapshot %(cgsnap)s not found when creating "
                              "consistency group %(cg)s from source."),
                          {'cg': name, 'cgsnap': cgsnapshot_id})
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Error occurred when creating consistency group"
                              " %(cg)s from cgsnapshot %(cgsnap)s."),
                          {'cg': name, 'cgsnap': cgsnapshot_id})

        # Update quota for consistencygroups
        self.update_quota(context, group, 1)

        if not group.host:
            msg = _("No host to create consistency group %s.") % group.id
            LOG.error(msg)
            raise exception.InvalidConsistencyGroup(reason=msg)

        if cgsnapshot_id:
            self._create_cg_from_cgsnapshot(context, group, cgsnapshot_id)
        elif source_cgid:
            self._create_cg_from_source_cg(context, group, source_cgid)

        return group

    def _create_cg_from_cgsnapshot(self, context, group, cgsnapshot_id):
        try:
            cgsnapshot = objects.CGSnapshot.get_by_id(context, cgsnapshot_id)
            snapshots = objects.SnapshotList.get_all_for_cgsnapshot(
                context, cgsnapshot.id)

            if not snapshots:
                msg = _("Cgsnahost is empty. No consistency group "
                        "will be created.")
                raise exception.InvalidConsistencyGroup(reason=msg)

            for snapshot in snapshots:
                kwargs = {}
                kwargs['availability_zone'] = group.availability_zone
                kwargs['cgsnapshot'] = cgsnapshot
                kwargs['consistencygroup'] = group
                kwargs['snapshot'] = snapshot
                volume_type_id = snapshot.volume_type_id
                if volume_type_id:
                    kwargs['volume_type'] = volume_types.get_volume_type(
                        context, volume_type_id)

                # Since cgsnapshot is passed in, the following call will
                # create a db entry for the volume, but will not call the
                # volume manager to create a real volume in the backend yet.
                # If error happens, taskflow will handle rollback of quota
                # and removal of volume entry in the db.
                try:
                    self.volume_api.create(context,
                                           snapshot.volume_size,
                                           None,
                                           None,
                                           **kwargs)
                except exception.CinderException:
                    with excutils.save_and_reraise_exception():
                        LOG.error(_LE("Error occurred when creating volume "
                                      "entry from snapshot in the process of "
                                      "creating consistency group %(group)s "
                                      "from cgsnapshot %(cgsnap)s."),
                                  {'group': group.id,
                                   'cgsnap': cgsnapshot.id})
        except Exception:
            with excutils.save_and_reraise_exception():
                try:
                    group.destroy()
                finally:
                    LOG.error(_LE("Error occurred when creating consistency "
                                  "group %(group)s from cgsnapshot "
                                  "%(cgsnap)s."),
                              {'group': group.id,
                               'cgsnap': cgsnapshot.id})

        volumes = self.db.volume_get_all_by_group(context,
                                                  group.id)
        for vol in volumes:
            # Update the host field for the volume.
            self.db.volume_update(context, vol['id'],
                                  {'host': group.get('host')})

        self.volume_rpcapi.create_consistencygroup_from_src(
            context, group, cgsnapshot)

    def _create_cg_from_source_cg(self, context, group, source_cgid):
        try:
            source_cg = objects.ConsistencyGroup.get_by_id(context,
                                                           source_cgid)
            source_vols = self.db.volume_get_all_by_group(context,
                                                          source_cg.id)

            if not source_vols:
                msg = _("Source CG is empty. No consistency group "
                        "will be created.")
                raise exception.InvalidConsistencyGroup(reason=msg)

            for source_vol in source_vols:
                kwargs = {}
                kwargs['availability_zone'] = group.availability_zone
                kwargs['source_cg'] = source_cg
                kwargs['consistencygroup'] = group
                kwargs['source_volume'] = source_vol
                volume_type_id = source_vol.get('volume_type_id')
                if volume_type_id:
                    kwargs['volume_type'] = volume_types.get_volume_type(
                        context, volume_type_id)

                # Since source_cg is passed in, the following call will
                # create a db entry for the volume, but will not call the
                # volume manager to create a real volume in the backend yet.
                # If error happens, taskflow will handle rollback of quota
                # and removal of volume entry in the db.
                try:
                    self.volume_api.create(context,
                                           source_vol['size'],