# Copyright (c) 2017 Veritas Technologies LLC.  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 mock

from cinder import context
from cinder import exception
from cinder import test
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.volume import configuration as conf
from cinder.volume.drivers.veritas import vrtshyperscale as vrts


class FakeDb(object):
    def volume_metadata_get(self, *a, **kw):
        return {}

    def volume_metadata_update(self, *a, **kw):
        return None


def _stub_volume(*args, **kwargs):
    updates = {'provider_location': 'hyperscale-sv:/hyperscale'}
    return fake_volume.fake_db_volume(**updates)


def _stub_snapshot(*args, **kwargs):
    updates = {'volume': _stub_volume(), 'name': 'vrts'}
    return fake_snapshot.fake_db_snapshot(**updates)


def _stub_stats():
    data = {}
    data["volume_backend_name"] = 'Veritas_HyperScale'
    data["vendor_name"] = 'Veritas Technologies LLC'
    data["driver_version"] = '1.0'
    data["storage_protocol"] = 'nfs'
    data['total_capacity_gb'] = 0.0
    data['free_capacity_gb'] = 0.0
    data['reserved_percentage'] = 0
    data['QoS_support'] = False
    return data


class VRTSHyperScaleDriverTestCase(test.TestCase):
    """Test case for Veritas HyperScale VolumeDriver."""

    driver_name = "cinder.volume.drivers.veritas.vrtshyperscale"

    @staticmethod
    def gvmv_side_effect(arg1, arg2):
        """Mock side effect for _get_volume_metadata_value."""
        # mock the return of get_volume_metadata_value
        # for different arguments
        if arg2 == 'Secondary_datanode_key':
            return '{9876}'
        elif arg2 == 'Secondary_datanode_ip':
            return '192.0.2.2'
        elif arg2 == 'current_dn_ip':
            return '192.0.2.1'
        elif arg2 == 'vsa_ip':
            return '192.0.2.1'

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._fetch_config_for_compute')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._fetch_config_for_datanode')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._fetch_config_for_controller')
    def setUp(self, mock_fcfcntr, mock_fcfd, mock_fcfc):
        mock_fcfcntr.return_value = None
        mock_fcfd.return_value = None
        mock_fcfc.return_value = None

        # Initialise a test seup
        super(VRTSHyperScaleDriverTestCase, self).setUp()

        self.configuration = mock.Mock(conf.Configuration(None))
        self.configuration.reserved_percentage = 0
        self.context = context.get_admin_context()
        self.driver = vrts.HyperScaleDriver(
            db=FakeDb(), configuration=self.configuration)
        self.driver.dn_routing_key = '{1234}'
        self.driver.datanode_ip = '192.0.2.1'
        self.volume = _stub_volume()
        self.snapshot = _stub_snapshot()

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_replicas')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_details_for_create_volume')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.api.API.update_volume_metadata')
    def test_create_volume_single_replicas(self, mock_uvm, mock_mdp,
                                           mock_gvdfcv, mock_get_replicas,
                                           mock_gvm):
        """Test single volume replica. Happy path test case."""
        # Mock volume meatadata
        mock_gvm.return_value = _stub_volume()

        # Mock number of replicas to 1
        mock_get_replicas.return_value = 1
        # assume volume details are populated correctly
        mock_gvdfcv.return_value = _stub_volume()

        # assume volume message is sent to data node successfully
        mock_mdp.return_value = ("", None)
        # assume that the volume metadata gets updated correctly
        mock_uvm.return_value = {}

        # declare the expected result
        expected_result = {
            'provider_location': 'hyperscale-sv:/hyperscale',
            'metadata': mock_gvm.return_value
        }

        # call create volume and get the result
        actual_result = self.driver.create_volume(self.volume)

        # Test if the return value matched the expected results
        self.assertDictEqual(actual_result, expected_result)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.get_hyperscale_version')
    def test_check_for_setup_error(self, mock_ghv):
        """Test check for setup errors in Veritas HyperScale driver.

        The test case checks happy path execution when driver version 1.0.0
        is installed.
        """
        mock_ghv.return_value = "1.0.0"

        # check the driver for setup errors
        self.driver.check_for_setup_error()

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.get_hyperscale_version')
    def test_check_for_setup_error_unsupported_version(self, mock_ghv):
        """Test check for setup errors in Veritas HyperScale driver.

        The test case checks happy path execution when driver version 1.0.0
        is installed.
        """
        mock_ghv.return_value = "1.0.0.1"

        # check the driver for setup errors
        self.assertRaises(exception.VolumeBackendAPIException,
                          self.driver.check_for_setup_error)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.get_hyperscale_version')
    def test_check_for_setup_error_exception(self, mock_ghv):
        """Test check for setup errors in Veritas HyperScale driver.

        The test case checks happy path execution when driver version 1.0.0
        is installed.
        """
        mock_ghv.side_effect = exception.ErrorInHyperScaleVersion(
            cmd_error="mock error")

        # check the driver for setup errors
        self.assertRaises(exception.VolumeBackendAPIException,
                          self.driver.check_for_setup_error)

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_delete_volume_no_replica(self, mock_mdp, mock_gvmv):
        """Test happy path for delete_volume one data nodes."""
        mock_gvmv.return_value = None
        self.driver.delete_volume(self.volume)

        message_body = {'display_name': self.volume['name']}

        mock_mdp.assert_called_with(self.driver.dn_routing_key,
                                    'hyperscale.storage.dm.volume.delete',
                                    **message_body)

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_delete_volume_more_than_one_replica(self, mock_mdp, mock_gvmv):
        """Test happy path for delete_volume with more than one data nodes."""
        mock_gvmv.side_effect = VRTSHyperScaleDriverTestCase.gvmv_side_effect

        message_body = {'display_name': self.volume['name']}

        # make the delete call
        self.driver.delete_volume(self.volume)

        # check if delete volume sent to reflection target on data node
        # check if mq message sent with 'Secondary_datanode_key'
        mock_mdp.assert_any_call(
            '{9876}', 'hyperscale.storage.dm.volume.delete', **message_body)

        # check if the delete is sent to primary data node as well
        mock_mdp.assert_any_call(self.driver.dn_routing_key,
                                 'hyperscale.storage.dm.volume.delete',
                                 **message_body)

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_delete_volume_no_replica_failure(self, mock_mdp, mock_gvmv):
        """Failure case for delete_volume one node in data plane."""
        mock_gvmv.side_effect = None
        self.driver.delete_volume(self.volume)
        mock_mdp.side_effect = exception.UnableToProcessHyperScaleCmdOutput(
            cmd_out='mock error')
        self.assertRaises(exception.VolumeIsBusy, self.driver.delete_volume,
                          self.volume)

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_delete_volume_more_than_one_replica_failure(self, mock_mdp,
                                                         mock_gvmv):
        """failure case for delete_volume with more than one data nodes."""
        mock_gvmv.side_effect = VRTSHyperScaleDriverTestCase.gvmv_side_effect

        mock_mdp.side_effect = exception.UnableToProcessHyperScaleCmdOutput(
            cmd_out='mock error')

        self.assertRaises(exception.VolumeIsBusy, self.driver.delete_volume,
                          self.volume)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.get_guid_with_curly_brackets')
    def test_delete_snapshot_force_flag(self, mock_ggwcb):
        """Test snapshot deletion does not happen if force flag is set."""
        # get a mock snapshot object
        snapshot = fake_snapshot.fake_db_snapshot()
        # set the force in metadata of snapshot
        snapshot['metadata'] = {"force": "force"}

        # call the delete volume
        self.driver.delete_snapshot(snapshot)

        # if snapshot has force set in metadata then
        # get_guid_with_curly_brackets() will not be called because we
        # return as soon as we see force
        mock_ggwcb.assert_not_called()

    def test_delete_snapshot_isbusy_flag(self):
        """Test snapshot deletion throws exception if snapshot is busy."""
        # get a mock snapshot object
        snapshot = fake_snapshot.fake_db_snapshot()
        # set the force in metadata of snapshot
        snapshot['metadata'] = {"is_busy": "is_busy"}

        # call the delete volume to check if it raises Busy Exception
        self.assertRaises(exception.SnapshotIsBusy,
                          self.driver.delete_snapshot, snapshot)

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata')
    @mock.patch('cinder.volume.api.API.get_volume')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_delete_snapshot_from_primary_dn(self, mock_mdp, mock_gv,
                                             mock_gvm):
        """Test snapshot deletion from primary DN."""
        # get mock volume
        mock_gv.return_value = None
        mock_gvm.return_value = {'current_dn_ip': self.driver.datanode_ip}

        message_body = {}
        message_body['volume_guid'] = '{' + self.volume['id'] + '}'
        message_body['snapshot_id'] = '{' + self.snapshot['id'] + '}'

        # call delete snapshot
        self.driver.delete_snapshot(self.snapshot)

        # assert msg is sent over mq with primary DN routing key
        mock_mdp.assert_called_with(self.driver.dn_routing_key,
                                    'hyperscale.storage.dm.version.delete',
                                    **message_body)

    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata')
    @mock.patch('cinder.volume.api.API.get_volume')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    def test_delete_snapshot_from_current_dn(self, mock_gvmv, mock_mdp,
                                             mock_gv, mock_gvm):
        """Test snapshot deletion DN value from volume."""
        # get a mock volume
        mock_gv.return_value = _stub_volume()

        # get a mock value of DN from volume
        mock_gvmv.return_value = '{9876}'

        message_body = {}
        message_body['volume_guid'] = '{' + self.volume['id'] + '}'
        message_body['snapshot_id'] = '{' + self.snapshot['id'] + '}'

        # call delete snapshot
        self.driver.delete_snapshot(self.snapshot)

        # assert msg is sent over mq with key from volume's current_dn_owner
        mock_mdp.assert_called_with(
            '{9876}', 'hyperscale.storage.dm.version.delete', **message_body)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_fetch_volume_stats_failure(self, mock_mdp):
        """Test case checking failure of pool for fetching stats."""
        # since we have initialised the pool to None in setup()
        # the function will return only the stub without populating
        # any free and used stats
        mock_obj = {'payload': {}}

        mock_mdp.return_value = (mock_obj, None)
        self.assertDictEqual(_stub_stats(), self.driver._fetch_volume_status())

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_create_cloned_volume_with_exception(self, mock_mdp):
        """Test case throws exception when command failed to execute."""
        vol_a = _stub_volume()
        vol_b = _stub_volume()
        mock_mdp.side_effect = exception.UnableToExecuteHyperScaleCmd(
            cmd_out='mock error')
        self.assertRaises(exception.UnableToExecuteHyperScaleCmd,
                          self.driver.create_cloned_volume, vol_b, vol_a)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale'
                '.HyperScaleDriver._select_rt')
    def test_create_cloned_volume_with_no_replica(self, mock_srt, mock_mdp):
        """Test case clone volume when there is no replica."""
        mock_obj = {'payload': {}}
        mock_mdp.return_value = (mock_obj, None)
        mock_srt.return_value = (None, None)
        vol_a = _stub_volume()
        vol_b = _stub_volume()
        self.assertDictContainsSubset({
            'provider_location': 'hyperscale-sv:/hyperscale'
        }, self.driver.create_cloned_volume(vol_b, vol_a))

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale'
                '.HyperScaleDriver._select_rt')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    def test_create_cloned_volume_with_replica(self, mock_gvmv, mock_srt,
                                               mock_mdp):
        """Test case clone volume when there is replica."""
        mock_gvmv.side_effect = VRTSHyperScaleDriverTestCase.gvmv_side_effect
        mock_obj = {'payload': {}}
        mock_mdp.return_value = (mock_obj, None)
        mock_srt.return_value = ('{1234}', '192.0.2.2')
        vol_a = _stub_volume()
        vol_b = _stub_volume()
        metadata = {
            'current_dn_owner': '{1234}',
            'Potential_secondary_key': '{1234}',
            'Primary_datanode_ip': '192.0.2.1',
            'Potential_secondary_ip': '192.0.2.2',
            'current_dn_ip': '192.0.2.1',
            'source_volid': vol_a['id'],
            'size': vol_a['size']
        }
        self.assertDictContainsSubset({
            'provider_location': 'hyperscale-sv:/hyperscale',
            'metadata': metadata
        }, self.driver.create_cloned_volume(vol_b, vol_a))

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_extend_volume_with_exception(self, mock_mdp):
        """Test case extend volume to the given size in GB."""
        mock_mdp.side_effect = exception.UnableToProcessHyperScaleCmdOutput(
            cmd_out='mock error')
        self.assertRaises(exception.VolumeDriverException,
                          self.driver.extend_volume, _stub_volume(), 256)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_extend_volume_no_exception(self, mock_mdp):
        """Test case extend volume thorws exception."""
        mock_mdp.return_value = (None, None)
        self.driver.extend_volume(_stub_volume(), 256)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    def test_create_volume_from_snapshot_with_exception(self, mock_mdp):
        """Test case create volume from snapshot thorws exception."""
        fake_volume, fake_snapshot = _stub_volume(), _stub_snapshot()
        mock_mdp.side_effect = exception.UnableToExecuteHyperScaleCmd(
            cmd_out='mock error')
        self.assertRaises(exception.UnableToExecuteHyperScaleCmd,
                          self.driver.create_volume_from_snapshot, fake_volume,
                          fake_snapshot)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale'
                '.HyperScaleDriver._select_rt')
    def test_create_volume_from_snapshot_with_no_replica(self, mock_srt,
                                                         mock_mdp):
        """Test case create volume from snapshot when there is no replica."""
        mock_obj = {'payload': {}}
        mock_mdp.return_value = (mock_obj, None)
        mock_srt.return_value = (None, None)
        fake_volume, fake_snapshot = _stub_volume(), _stub_snapshot()
        self.assertDictContainsSubset({
            'provider_location': 'hyperscale-sv:/hyperscale'
        }, self.driver.create_volume_from_snapshot(fake_volume, fake_snapshot))

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale'
                '.HyperScaleDriver._select_rt')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    def test_create_volume_from_snapshot_with_replica(self, mock_gvmv,
                                                      mock_srt, mock_mdp):
        """Test case create volume from snapshot when there is replica."""
        mock_gvmv.side_effect = VRTSHyperScaleDriverTestCase.gvmv_side_effect
        mock_obj = {'payload': {}}
        mock_mdp.return_value = (mock_obj, None)
        mock_srt.return_value = ('{1234}', '192.0.2.2')
        fake_volume, fake_snapshot = _stub_volume(), _stub_snapshot()
        metadata = {
            'current_dn_owner': '{1234}',
            'Potential_secondary_key': '{1234}',
            'Primary_datanode_ip': '192.0.2.1',
            'Potential_secondary_ip': '192.0.2.2',
            'current_dn_ip': '192.0.2.1',
            'snapshot_id': fake_snapshot['id'],
            'parent_volume_guid': '{' + fake_snapshot['volume']['id'] + '}'
        }
        self.assertDictContainsSubset({
            'provider_location': 'hyperscale-sv:/hyperscale',
            'metadata': metadata
        }, self.driver.create_volume_from_snapshot(fake_volume, fake_snapshot))

    def test_initialize_connection(self):
        """Test case intialize_connection."""
        fake_volume = _stub_volume()
        expected_data = {
            'driver_volume_type': 'veritas_hyperscale',
            'data': {
                'export': fake_volume['provider_location'],
                'name': fake_volume['name']
            }
        }
        self.assertEqual(expected_data,
                         self.driver.initialize_connection(fake_volume, None))

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_compute_plane')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.episodic_snap')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    def test_create_snapshot_with_exception(
            self, mock_gvmv, mock_es, mock_mcp):
        """Test case create snapshot throws exception."""
        mock_gvmv.side_effect = VRTSHyperScaleDriverTestCase.gvmv_side_effect
        mock_es_obj = {'payload': {'update': False}}
        mock_es.return_value = mock_es_obj
        mock_mcp.side_effect = exception.UnableToExecuteHyperScaleCmd(
            cmd_out='mock error')
        fake_snapshot = _stub_snapshot()
        self.assertRaises(exception.UnableToExecuteHyperScaleCmd,
                          self.driver.create_snapshot, fake_snapshot)

    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_controller')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_data_plane')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.episodic_snap')
    @mock.patch('cinder.volume.drivers.veritas.vrtshyperscale.HyperScaleDriver'
                '._get_volume_metadata_value')
    @mock.patch('cinder.volume.drivers.veritas.utils'
                '.message_compute_plane')
    def test_create_snapshot_user(
            self, mock_cdp, mock_gvmv, mock_es, mock_mdp, mock_mc):
        """Test case user snapshot."""
        mock_gvmv.side_effect = VRTSHyperScaleDriverTestCase.gvmv_side_effect
        mock_es_obj = {'payload': {'update': False}}
        mock_es.return_value = mock_es_obj
        mock_obj = {'payload': {}}
        mock_mdp.return_value = ("", None)
        mock_mc.return_value = ("", None)
        mock_cdp.return_value = (mock_obj, None)
        fake_snapshot = _stub_snapshot()
        expected = {
            'metadata': {
                'status': 'creating',
                'datanode_ip': '192.0.2.1',
                'TYPE': vrts.TYPE_USER_SNAP
            }
        }
        self.assertEqual(expected, self.driver.create_snapshot(fake_snapshot))
