# Copyright 2014 IBM Corp.
# 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.
#

import random

from eventlet import greenthread
from oslo_concurrency import processutils
from oslo_log import log as logging
from oslo_utils import excutils
import six

from cinder import exception
from cinder.i18n import _
from cinder import ssh_utils
from cinder import utils
from cinder.volume.drivers.ibm.storwize_svc import storwize_const

LOG = logging.getLogger(__name__)


class StorwizeSVCReplication(object):
    def __init__(self, driver, replication_target=None):
        self.driver = driver
        self.target = replication_target or {}

    def failover_volume_host(self, context, vref):
        pass

    def replication_failback(self, volume):
        pass

    def volume_replication_setup(self, context, vref):
        pass


class StorwizeSVCReplicationGlobalMirror(StorwizeSVCReplication):
    """Support for Storwize/SVC global mirror mode replication.

    Global Mirror establishes a Global Mirror relationship between
    two volumes of equal size. The volumes in a Global Mirror relationship
    are referred to as the master (source) volume and the auxiliary
    (target) volume. This mode is dedicated to the asynchronous volume
    replication.
    """

    asyncmirror = True

    def __init__(self, driver, replication_target=None, target_helpers=None):
        super(StorwizeSVCReplicationGlobalMirror, self).__init__(
            driver, replication_target)
        self.target_helpers = target_helpers

    def volume_replication_setup(self, context, vref):
        LOG.debug('enter: volume_replication_setup: volume %s', vref['name'])

        target_vol_name = storwize_const.REPLICA_AUX_VOL_PREFIX + vref['name']
        try:
            attr = self.target_helpers.get_vdisk_attributes(target_vol_name)
            if not attr:
                opts = self.driver._get_vdisk_params(vref['volume_type_id'])
                pool = self.target.get('pool_name')
                src_attr = self.driver._helpers.get_vdisk_attributes(
                    vref['name'])
                opts['iogrp'] = src_attr['IO_group_id']
                self.target_helpers.create_vdisk(target_vol_name,
                                                 six.text_type(vref['size']),
                                                 'gb', pool, opts)

            system_info = self.target_helpers.get_system_info()
            self.driver._helpers.create_relationship(
                vref['name'], target_vol_name, system_info.get('system_name'),
                self.asyncmirror)
        except Exception as e:
            msg = (_("Unable to set up mirror mode replication for %(vol)s. "
                     "Exception: %(err)s.") % {'vol': vref['id'],
                                               'err': e})
            LOG.exception(msg)
            raise exception.VolumeDriverException(message=msg)
        LOG.debug('leave: volume_replication_setup:volume %s', vref['name'])

    def failover_volume_host(self, context, vref):
        LOG.debug('enter: failover_volume_host: vref=%(vref)s',
                  {'vref': vref['name']})
        target_vol = storwize_const.REPLICA_AUX_VOL_PREFIX + vref['name']

        try:
            rel_info = self.target_helpers.get_relationship_info(target_vol)
            # Reverse the role of the primary and secondary volumes
            self.target_helpers.switch_relationship(rel_info['name'])
            return
        except Exception as e:
            LOG.exception('Unable to fail-over the volume %(id)s to the '
                          'secondary back-end by switchrcrelationship '
                          'command, error: %(error)s',
                          {"id": vref['id'], "error": e})
            # If the switch command fail, try to make the aux volume
            # writeable again.
            try:
                self.target_helpers.stop_relationship(target_vol,
                                                      access=True)
                try:
                    self.target_helpers.start_relationship(target_vol, 'aux')
                except exception.VolumeBackendAPIException as e:
                    LOG.error(
                        'Error running startrcrelationship due to %(err)s.',
                        {'err': e})
                return
            except Exception as e:
                msg = (_('Unable to fail-over the volume %(id)s to the '
                         'secondary back-end, error: %(error)s') %
                       {"id": vref['id'], "error": e})
                LOG.exception(msg)
                raise exception.VolumeDriverException(message=msg)

    def replication_failback(self, volume):
        tgt_volume = storwize_const.REPLICA_AUX_VOL_PREFIX + volume['name']
        rel_info = self.target_helpers.get_relationship_info(tgt_volume)
        if rel_info:
            try:
                self.target_helpers.switch_relationship(rel_info['name'],
                                                        aux=False)
                return
            except Exception as e:
                msg = (_('Unable to fail-back the volume:%(vol)s to the '
                         'master back-end, error:%(error)s') %
                       {"vol": volume['name'], "error": e})
                LOG.exception(msg)
                raise exception.VolumeDriverException(message=msg)


class StorwizeSVCReplicationMetroMirror(
        StorwizeSVCReplicationGlobalMirror):
    """Support for Storwize/SVC metro mirror mode replication.

    Metro Mirror establishes a Metro Mirror relationship between
    two volumes of equal size. The volumes in a Metro Mirror relationship
    are referred to as the master (source) volume and the auxiliary
    (target) volume.
    """

    asyncmirror = False

    def __init__(self, driver, replication_target=None, target_helpers=None):
        super(StorwizeSVCReplicationMetroMirror, self).__init__(
            driver, replication_target, target_helpers)


class StorwizeSVCReplicationGMCV(StorwizeSVCReplicationGlobalMirror):
    """Support for Storwize/SVC global mirror with change volumes mode replication.

    Global Mirror with Change Volumes(GMCV) provides asynchronous replication
    based on point-in-time copies of data. The volumes in a GMCV relationship
    are referred to as the master (source) volume, master change volume, the
    auxiliary (target) volume and auxiliary change volume.
    """

    asyncmirror = True

    def __init__(self, driver, replication_target=None, target_helpers=None):
        super(StorwizeSVCReplicationGMCV, self).__init__(
            driver, replication_target, target_helpers)

    def volume_replication_setup(self, context, vref):
        LOG.debug('enter: volume_replication_setup: volume %s', vref['name'])
        source_change_vol_name = (storwize_const.REPLICA_CHG_VOL_PREFIX +
                                  vref['name'])
        target_vol_name = storwize_const.REPLICA_AUX_VOL_PREFIX + vref['name']
        target_change_vol_name = (storwize_const.REPLICA_CHG_VOL_PREFIX +
                                  target_vol_name)
        try:
            src_attr = self.driver._helpers.get_vdisk_attributes(
                vref['name'])
            # Create source change volume if it doesn't exist
            src_change_attr = self.driver._helpers.get_vdisk_attributes(
                source_change_vol_name)
            if not src_change_attr:
                src_change_opts = self.driver._get_vdisk_params(
                    vref['volume_type_id'])
                src_change_opts['iogrp'] = src_attr['IO_group_id']
                # Change volumes would usually be thin-provisioned
                src_change_opts['autoexpand'] = True
                self.driver._helpers.create_vdisk(source_change_vol_name,
                                                  six.text_type(vref['size']),
                                                  'gb',
                                                  src_attr['mdisk_grp_id'],
                                                  src_change_opts)
            # Create target volume if it doesn't exist
            target_attr = self.target_helpers.get_vdisk_attributes(
                target_vol_name)
            if not target_attr:
                target_opts = self.driver._get_vdisk_params(
                    vref['volume_type_id'])
                target_pool = self.target.get('pool_name')
                target_opts['iogrp'] = src_attr['IO_group_id']
                self.target_helpers.create_vdisk(target_vol_name,
                                                 six.text_type(vref['size']),
                                                 'gb',
                                                 target_pool,
                                                 target_opts)

            # Create target change volume if it doesn't exist
            target_change_attr = self.target_helpers.get_vdisk_attributes(
                target_change_vol_name)
            if not target_change_attr:
                target_change_opts = self.driver._get_vdisk_params(
                    vref['volume_type_id'])
                target_change_pool = self.target.get('pool_name')
                target_change_opts['iogrp'] = src_attr['IO_group_id']
                # Change Volumes would usually be thin-provisioned
                target_change_opts['autoexpand'] = True
                self.target_helpers.create_vdisk(target_change_vol_name,
                                                 six.text_type(vref['size']),
                                                 'gb',
                                                 target_change_pool,
                                                 target_change_opts)

            system_info = self.target_helpers.get_system_info()
            # Get cycle_period_seconds
            src_change_opts = self.driver._get_vdisk_params(
                vref['volume_type_id'])
            cycle_period_seconds = src_change_opts.get('cycle_period_seconds')
            self.driver._helpers.create_relationship(
                vref['name'], target_vol_name, system_info.get('system_name'),
                self.asyncmirror, True, source_change_vol_name,
                cycle_period_seconds)
            # Set target change volume
            self.target_helpers.change_relationship_changevolume(
                target_vol_name, target_change_vol_name, False)
            # Start gmcv relationship
            self.driver._helpers.start_relationship(vref['name'])
        except Exception as e:
            msg = (_("Unable to set up gmcv mode replication for %(vol)s. "
                     "Exception: %(err)s.") % {'vol': vref['id'],
                                               'err': six.text_type(e)})
            LOG.exception(msg)
            raise exception.VolumeDriverException(message=msg)
        LOG.debug('leave: volume_replication_setup:volume %s', vref['name'])

    def failover_volume_host(self, context, vref):
        LOG.debug('enter: failover_volume_host: vref=%(vref)s',
                  {'vref': vref['name']})
        # Make the aux volume writeable.
        try:
            tgt_volume = storwize_const.REPLICA_AUX_VOL_PREFIX + vref.name
            self.target_helpers.stop_relationship(tgt_volume, access=True)
            try:
                self.target_helpers.start_relationship(tgt_volume, 'aux')
            except exception.VolumeBackendAPIException as e:
                LOG.error('Error running startrcrelationship due to %(err)s.',
                          {'err': e})
            return
        except Exception as e:
            msg = (_('Unable to fail-over the volume %(id)s to the '
                   'secondary back-end, error: %(error)s') %
                   {"id": vref['id'], "error": six.text_type(e)})
            LOG.exception(msg)
            raise exception.VolumeDriverException(message=msg)

    def replication_failback(self, volume):
        LOG.debug('enter: replication_failback: volume=%(volume)s',
                  {'volume': volume['name']})
        tgt_volume = storwize_const.REPLICA_AUX_VOL_PREFIX + volume['name']
        rel_info = self.target_helpers.get_relationship_info(tgt_volume)
        if rel_info:
            try:
                self.target_helpers.stop_relationship(tgt_volume, access=True)
                self.target_helpers.start_relationship(tgt_volume, 'master')
                return
            except Exception as e:
                msg = (_('Unable to fail-back the volume:%(vol)s to the '
                         'master back-end, error:%(error)s') %
                       {"vol": volume['name'], "error": six.text_type(e)})
                LOG.exception(msg)
                raise exception.VolumeDriverException(message=msg)


class StorwizeSVCReplicationManager(object):

    def __init__(self, driver, replication_target=None, target_helpers=None):
        self.sshpool = None
        self.driver = driver
        self.target = replication_target
        self.target_helpers = target_helpers(self._run_ssh)
        self._master_helpers = self.driver._master_backend_helpers
        self.global_m = StorwizeSVCReplicationGlobalMirror(
            self.driver, replication_target, self.target_helpers)
        self.metro_m = StorwizeSVCReplicationMetroMirror(
            self.driver, replication_target, self.target_helpers)
        self.gmcv = StorwizeSVCReplicationGMCV(
            self.driver, replication_target, self.target_helpers)

    def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
        utils.check_ssh_injection(cmd_list)
        # TODO(vhou): We'll have a common method in ssh_utils to take
        # care of this _run_ssh method.
        command = ' '. join(cmd_list)

        if not self.sshpool:
            self.sshpool = ssh_utils.SSHPool(
                self.target.get('san_ip'),
                self.target.get('san_ssh_port', 22),
                self.target.get('ssh_conn_timeout', 30),
                self.target.get('san_login'),
                password=self.target.get('san_password'),
                privatekey=self.target.get('san_private_key', ''),
                min_size=self.target.get('ssh_min_pool_conn', 1),
                max_size=self.target.get('ssh_max_pool_conn', 5),)
        last_exception = None
        try:
            with self.sshpool.item() as ssh:
                while attempts > 0:
                    attempts -= 1
                    try:
                        return processutils.ssh_execute(
                            ssh, command, check_exit_code=check_exit_code)
                    except Exception as e:
                        LOG.error(six.text_type(e))
                        last_exception = e
                        greenthread.sleep(random.randint(20, 500) / 100.0)
                try:
                    raise processutils.ProcessExecutionError(
                        exit_code=last_exception.exit_code,
                        stdout=last_exception.stdout,
                        stderr=last_exception.stderr,
                        cmd=last_exception.cmd)
                except AttributeError:
                    raise processutils.ProcessExecutionError(
                        exit_code=-1, stdout="",
                        stderr="Error running SSH command",
                        cmd=command)
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.error("Error running SSH command: %s", command)

    def get_target_helpers(self):
        return self.target_helpers

    def get_replica_obj(self, rep_type):
        if rep_type == storwize_const.GLOBAL:
            return self.global_m
        elif rep_type == storwize_const.METRO:
            return self.metro_m
        elif rep_type == storwize_const.GMCV:
            return self.gmcv
        else:
            return None

    def _partnership_validate_create(self, client, remote_name, remote_ip):
        try:
            partnership_info = client.get_partnership_info(
                remote_name)
            if not partnership_info:
                candidate_info = client.get_partnershipcandidate_info(
                    remote_name)
                if candidate_info:
                    client.mkfcpartnership(remote_name)
                else:
                    client.mkippartnership(remote_ip)
        except Exception:
            msg = (_('Unable to establish the partnership with '
                     'the Storwize cluster %s.'), remote_name)
            LOG.error(msg)
            raise exception.VolumeDriverException(message=msg)

    def _partnership_start(self, client, remote_name):
        try:
            partnership_info = client.get_partnership_info(
                remote_name)
            if (partnership_info and
                    partnership_info['partnership'] != 'fully_configured'):
                client.chpartnership(partnership_info['id'])
        except Exception:
            msg = (_('Unable to start the partnership with '
                     'the Storwize cluster %s.'), remote_name)
            LOG.error(msg)
            raise exception.VolumeDriverException(message=msg)

    def establish_target_partnership(self):
        local_system_info = self._master_helpers.get_system_info()
        target_system_info = self.target_helpers.get_system_info()
        local_system_name = local_system_info['system_name']
        target_system_name = target_system_info['system_name']
        local_ip = self.driver.configuration.safe_get('san_ip')
        target_ip = self.target.get('san_ip')
        # Establish partnership only when the local system and the replication
        # target system is different.
        if target_system_name != local_system_name:
            self._partnership_validate_create(self._master_helpers,
                                              target_system_name, target_ip)
            self._partnership_validate_create(self.target_helpers,
                                              local_system_name, local_ip)
            self._partnership_start(self._master_helpers, target_system_name)
            self._partnership_start(self.target_helpers, local_system_name)
