# Copyright 2012 Pedro Navarro Perez
# Copyright 2015 Cloudbase Solutions SRL
# 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.

"""
Unit tests for Windows Server 2012 OpenStack Cinder volume driver
"""

import os
from unittest import mock

import ddt
from oslo_utils import fileutils
from oslo_utils import timeutils
from oslo_utils import units

from cinder import context
from cinder import exception
from cinder.image import image_utils
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit import test
from cinder.tests.unit import utils as test_utils
from cinder.tests.unit.windows import db_fakes
from cinder.volume import configuration as conf
from cinder.volume.drivers.windows import iscsi as windows_iscsi


@ddt.ddt
class TestWindowsISCSIDriver(test.TestCase):
    @mock.patch.object(windows_iscsi, 'utilsfactory')
    def setUp(self, mock_utilsfactory):
        super(TestWindowsISCSIDriver, self).setUp()
        self.configuration = conf.Configuration(None)
        self.configuration.append_config_values(windows_iscsi.windows_opts)
        self.flags(windows_iscsi_lun_path='fake_iscsi_lun_path')
        self.flags(image_conversion_dir='fake_image_conversion_dir')

        self._driver = windows_iscsi.WindowsISCSIDriver(
            configuration=self.configuration)

        self._context = context.get_admin_context()
        self.updated_at = timeutils.utcnow()

    @mock.patch.object(fileutils, 'ensure_tree')
    def test_do_setup(self, mock_ensure_tree):
        self._driver.do_setup(mock.sentinel.context)

        mock_ensure_tree.assert_has_calls(
            [mock.call('fake_iscsi_lun_path'),
             mock.call('fake_image_conversion_dir')])

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_portals')
    def test_check_for_setup_error(self, mock_get_portals):
        self._driver.check_for_setup_error()

        mock_get_portals.assert_called_once_with()

    @ddt.data(True, False)
    def test_get_portals(self, portals_available=True):
        iscsi_port = mock.sentinel.iscsi_port
        available_ips = ['fake_ip0', 'fake_ip1', 'fake_unrequested_ip']
        requested_ips = available_ips[:-1] + ['fake_inexistent_ips']

        available_portals = ([":".join([ip_addr, str(iscsi_port)])
                              for ip_addr in available_ips]
                             if portals_available else [])

        self._driver.configuration = mock.Mock()
        self._driver.configuration.target_port = iscsi_port
        self._driver.configuration.target_ip_address = requested_ips[0]
        self._driver.configuration.target_secondary_ip_addresses = (
            requested_ips[1:])

        self._driver._tgt_utils.get_portal_locations.return_value = (
            available_portals)

        if portals_available:
            portals = self._driver._get_portals()
            self.assertEqual(set(available_portals[:-1]), set(portals))
        else:
            self.assertRaises(exception.VolumeDriverException,
                              self._driver._get_portals)

        self._driver._tgt_utils.get_portal_locations.assert_called_once_with(
            available_only=True,
            fail_if_none_found=True)

    @ddt.data(True, False)
    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_portals')
    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_target_name')
    def test_get_host_information(self, multipath, mock_get_target_name,
                                  mock_get_portals):
        tgt_utils = self._driver._tgt_utils

        fake_auth_meth = 'CHAP'
        fake_chap_username = 'fake_chap_username'
        fake_chap_password = 'fake_chap_password'
        fake_target_iqn = 'fake_target_iqn'
        fake_host_info = {'target_iqn': 'fake_target_iqn',
                          'fake_prop': 'fake_value'}
        fake_provider_auth = "%s %s %s" % (fake_auth_meth,
                                           fake_chap_username,
                                           fake_chap_password)
        fake_portals = [mock.sentinel.portal_location0,
                        mock.sentinel.portal_location1]

        volume = fake_volume.fake_volume_obj(mock.sentinel.context,
                                             provider_auth=fake_provider_auth)

        mock_get_target_name.return_value = mock.sentinel.target_name
        mock_get_portals.return_value = fake_portals
        tgt_utils.get_target_information.return_value = fake_host_info

        expected_host_info = dict(fake_host_info,
                                  auth_method=fake_auth_meth,
                                  auth_username=fake_chap_username,
                                  auth_password=fake_chap_password,
                                  target_discovered=False,
                                  target_portal=fake_portals[0],
                                  target_lun=0,
                                  volume_id=volume.id)
        if multipath:
            expected_host_info['target_portals'] = fake_portals
            expected_host_info['target_iqns'] = [fake_target_iqn] * 2
            expected_host_info['target_luns'] = [0] * 2

        host_info = self._driver._get_host_information(volume, multipath)

        self.assertEqual(expected_host_info, host_info)

        mock_get_target_name.assert_called_once_with(volume)
        mock_get_portals.assert_called_once_with()
        tgt_utils.get_target_information.assert_called_once_with(
            mock.sentinel.target_name)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver,
                       '_get_host_information')
    def test_initialize_connection(self, mock_get_host_info):
        tgt_utils = self._driver._tgt_utils

        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
        fake_initiator = db_fakes.get_fake_connector_info()
        fake_initiator['multipath'] = mock.sentinel.multipath
        fake_host_info = {'fake_host_prop': 'fake_value'}

        mock_get_host_info.return_value = fake_host_info

        expected_conn_info = {'driver_volume_type': 'iscsi',
                              'data': fake_host_info}
        conn_info = self._driver.initialize_connection(volume,
                                                       fake_initiator)

        self.assertEqual(expected_conn_info, conn_info)
        mock_get_host_info.assert_called_once_with(
            volume, mock.sentinel.multipath)
        mock_associate = tgt_utils.associate_initiator_with_iscsi_target
        mock_associate.assert_called_once_with(
            fake_initiator['initiator'],
            volume.provider_location)

    def test_terminate_connection(self):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
        fake_initiator = db_fakes.get_fake_connector_info()

        self._driver.terminate_connection(volume, fake_initiator)

        self._driver._tgt_utils.deassociate_initiator.assert_called_once_with(
            fake_initiator['initiator'], volume.provider_location)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
    def test_create_volume(self, mock_local_path):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)

        self._driver.create_volume(volume)

        mock_local_path.assert_called_once_with(volume)
        self._driver._tgt_utils.create_wt_disk.assert_called_once_with(
            mock_local_path.return_value,
            volume.name,
            size_mb=volume.size * 1024)

    def test_local_path(self):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)

        fake_lun_path = 'fake_lun_path'
        self.flags(windows_iscsi_lun_path=fake_lun_path)

        disk_format = 'vhd'
        mock_get_fmt = self._driver._tgt_utils.get_supported_disk_format
        mock_get_fmt.return_value = disk_format

        disk_path = self._driver.local_path(volume)

        expected_fname = "%s.%s" % (volume.name, disk_format)
        expected_disk_path = os.path.join(fake_lun_path,
                                          expected_fname)
        self.assertEqual(expected_disk_path, disk_path)
        mock_get_fmt.assert_called_once_with()

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
    @mock.patch.object(fileutils, 'delete_if_exists')
    def test_delete_volume(self, mock_delete_if_exists, mock_local_path):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)

        self._driver.delete_volume(volume)

        mock_local_path.assert_called_once_with(volume)
        self._driver._tgt_utils.remove_wt_disk.assert_called_once_with(
            volume.name)
        mock_delete_if_exists.assert_called_once_with(
            mock_local_path.return_value)

    def test_create_snapshot(self):
        volume = fake_volume.fake_volume_obj(context.get_admin_context())
        snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context(),
                                                   volume_id=volume.id)
        snapshot.volume = volume

        self._driver.create_snapshot(snapshot)

        self._driver._tgt_utils.create_snapshot.assert_called_once_with(
            snapshot.volume_name, snapshot.name)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
    def test_create_volume_from_snapshot(self, mock_local_path):
        volume = fake_volume.fake_volume_obj(context.get_admin_context())
        snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context())
        snapshot.volume = volume

        self._driver.create_volume_from_snapshot(volume, snapshot)

        self._driver._tgt_utils.export_snapshot.assert_called_once_with(
            snapshot.name, mock_local_path.return_value)
        self._driver._tgt_utils.import_wt_disk.assert_called_once_with(
            mock_local_path.return_value, volume.name)

    def test_delete_snapshot(self):
        snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context())

        self._driver.delete_snapshot(snapshot)

        self._driver._tgt_utils.delete_snapshot.assert_called_once_with(
            snapshot.name)

    def test_get_target_name(self):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
        expected_target_name = "%s%s" % (
            self._driver.configuration.target_prefix,
            volume.name)

        target_name = self._driver._get_target_name(volume)
        self.assertEqual(expected_target_name, target_name)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_target_name')
    @mock.patch.object(windows_iscsi.volume_utils, 'generate_username')
    @mock.patch.object(windows_iscsi.volume_utils, 'generate_password')
    def test_create_export(self, mock_generate_password,
                           mock_generate_username,
                           mock_get_target_name):
        tgt_utils = self._driver._tgt_utils
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
        self._driver.configuration.chap_username = None
        self._driver.configuration.chap_password = None
        self._driver.configuration.use_chap_auth = True
        fake_chap_username = 'fake_chap_username'
        fake_chap_password = 'fake_chap_password'

        mock_get_target_name.return_value = mock.sentinel.target_name
        mock_generate_username.return_value = fake_chap_username
        mock_generate_password.return_value = fake_chap_password
        tgt_utils.iscsi_target_exists.return_value = False

        vol_updates = self._driver.create_export(mock.sentinel.context,
                                                 volume,
                                                 mock.sentinel.connector)

        mock_get_target_name.assert_called_once_with(volume)
        tgt_utils.iscsi_target_exists.assert_called_once_with(
            mock.sentinel.target_name)
        tgt_utils.set_chap_credentials.assert_called_once_with(
            mock.sentinel.target_name,
            fake_chap_username,
            fake_chap_password)
        tgt_utils.add_disk_to_target.assert_called_once_with(
            volume.name, mock.sentinel.target_name)

        expected_provider_auth = ' '.join(('CHAP',
                                           fake_chap_username,
                                           fake_chap_password))
        expected_vol_updates = dict(
            provider_location=mock.sentinel.target_name,
            provider_auth=expected_provider_auth)
        self.assertEqual(expected_vol_updates, vol_updates)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_target_name')
    def test_remove_export(self, mock_get_target_name):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)

        self._driver.remove_export(mock.sentinel.context, volume)

        mock_get_target_name.assert_called_once_with(volume)
        self._driver._tgt_utils.delete_iscsi_target.assert_called_once_with(
            mock_get_target_name.return_value)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
    @mock.patch.object(image_utils, 'temporary_file')
    @mock.patch.object(image_utils, 'fetch_to_vhd')
    @mock.patch('os.unlink')
    def test_copy_image_to_volume(self, mock_unlink, mock_fetch_to_vhd,
                                  mock_tmp_file, mock_local_path):
        tgt_utils = self._driver._tgt_utils
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)

        mock_tmp_file.return_value.__enter__.return_value = (
            mock.sentinel.tmp_vhd_path)
        mock_local_path.return_value = mock.sentinel.vol_vhd_path

        self._driver.copy_image_to_volume(mock.sentinel.context,
                                          volume,
                                          mock.sentinel.image_service,
                                          mock.sentinel.image_id)

        mock_local_path.assert_called_once_with(volume)
        mock_tmp_file.assert_called_once_with(suffix='.vhd')
        image_utils.fetch_to_vhd.assert_called_once_with(
            mock.sentinel.context, mock.sentinel.image_service,
            mock.sentinel.image_id, mock.sentinel.tmp_vhd_path,
            self._driver.configuration.volume_dd_blocksize,
            disable_sparse=False)

        mock_unlink.assert_called_once_with(mock.sentinel.vol_vhd_path)
        self._driver._vhdutils.convert_vhd.assert_called_once_with(
            mock.sentinel.tmp_vhd_path,
            mock.sentinel.vol_vhd_path,
            tgt_utils.get_supported_vhd_type.return_value)
        self._driver._vhdutils.resize_vhd.assert_called_once_with(
            mock.sentinel.vol_vhd_path,
            volume.size * units.Gi,
            is_file_max_size=False)

        tgt_utils.change_wt_disk_status.assert_has_calls(
            [mock.call(volume.name, enabled=False),
             mock.call(volume.name, enabled=True)])

    @mock.patch.object(windows_iscsi.uuidutils, 'generate_uuid')
    def test_temporary_snapshot(self, mock_generate_uuid):
        tgt_utils = self._driver._tgt_utils
        mock_generate_uuid.return_value = mock.sentinel.snap_uuid
        expected_snap_name = '%s-tmp-snapshot-%s' % (
            mock.sentinel.volume_name, mock.sentinel.snap_uuid)

        with self._driver._temporary_snapshot(
                mock.sentinel.volume_name) as snap_name:
            self.assertEqual(expected_snap_name, snap_name)
            tgt_utils.create_snapshot.assert_called_once_with(
                mock.sentinel.volume_name, expected_snap_name)

        tgt_utils.delete_snapshot.assert_called_once_with(
            expected_snap_name)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_temporary_snapshot')
    @mock.patch.object(image_utils, 'upload_volume')
    @mock.patch.object(fileutils, 'delete_if_exists')
    def test_copy_volume_to_image(self, mock_delete_if_exists,
                                  mock_upload_volume,
                                  mock_tmp_snap):
        tgt_utils = self._driver._tgt_utils

        disk_format = 'vhd'
        fake_image_meta = db_fakes.get_fake_image_meta()

        fake_volume = test_utils.create_volume(
            self._context, volume_type_id=fake.VOLUME_TYPE_ID,
            updated_at=self.updated_at)

        extra_specs = {
            'image_service:store_id': 'fake-store'
        }
        test_utils.create_volume_type(self._context.elevated(),
                                      id=fake.VOLUME_TYPE_ID, name="test_type",
                                      extra_specs=extra_specs)

        fake_img_conv_dir = 'fake_img_conv_dir'
        self.flags(image_conversion_dir=fake_img_conv_dir)

        tgt_utils.get_supported_disk_format.return_value = disk_format
        mock_tmp_snap.return_value.__enter__.return_value = (
            mock.sentinel.tmp_snap_name)

        expected_tmp_vhd_path = os.path.join(
            fake_img_conv_dir,
            fake_image_meta['id'] + '.' + disk_format)

        self._driver.copy_volume_to_image(
            mock.sentinel.context, fake_volume,
            mock.sentinel.image_service,
            fake_image_meta)

        mock_tmp_snap.assert_called_once_with(fake_volume.name)
        tgt_utils.export_snapshot.assert_called_once_with(
            mock.sentinel.tmp_snap_name,
            expected_tmp_vhd_path)
        mock_upload_volume.assert_called_once_with(
            mock.sentinel.context, mock.sentinel.image_service,
            fake_image_meta, expected_tmp_vhd_path, volume_fd=None,
            volume_format='vhd',
            store_id='fake-store', base_image_ref=None,
            compress=True, run_as_root=True)
        mock_delete_if_exists.assert_called_once_with(
            expected_tmp_vhd_path)

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_temporary_snapshot')
    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
    def test_create_cloned_volume(self, mock_local_path,
                                  mock_tmp_snap):
        tgt_utils = self._driver._tgt_utils

        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
        src_volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)

        mock_tmp_snap.return_value.__enter__.return_value = (
            mock.sentinel.tmp_snap_name)
        mock_local_path.return_value = mock.sentinel.vol_vhd_path

        self._driver.create_cloned_volume(volume, src_volume)

        mock_tmp_snap.assert_called_once_with(src_volume.name)
        tgt_utils.export_snapshot.assert_called_once_with(
            mock.sentinel.tmp_snap_name,
            mock.sentinel.vol_vhd_path)
        self._driver._vhdutils.resize_vhd.assert_called_once_with(
            mock.sentinel.vol_vhd_path, volume.size * units.Gi,
            is_file_max_size=False)
        tgt_utils.import_wt_disk.assert_called_once_with(
            mock.sentinel.vol_vhd_path, volume.name)

    @mock.patch('os.path.splitdrive')
    def test_get_capacity_info(self, mock_splitdrive):
        mock_splitdrive.return_value = (mock.sentinel.drive,
                                        mock.sentinel.path_tail)
        fake_size_gb = 2
        fake_free_space_gb = 1
        self._driver._hostutils.get_volume_info.return_value = (
            fake_size_gb * units.Gi,
            fake_free_space_gb * units.Gi)

        total_gb, free_gb = self._driver._get_capacity_info()

        self.assertEqual(fake_size_gb, total_gb)
        self.assertEqual(fake_free_space_gb, free_gb)

        self._driver._hostutils.get_volume_info.assert_called_once_with(
            mock.sentinel.drive)
        mock_splitdrive.assert_called_once_with('fake_iscsi_lun_path')

    @mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_capacity_info')
    def test_update_volume_stats(self, mock_get_capacity_info):
        mock_get_capacity_info.return_value = (
            mock.sentinel.size_gb,
            mock.sentinel.free_space_gb)

        self.flags(volume_backend_name='volume_backend_name')
        self.flags(reserved_percentage=10)

        expected_volume_stats = dict(
            volume_backend_name='volume_backend_name',
            vendor_name='Microsoft',
            driver_version=self._driver.VERSION,
            storage_protocol='iSCSI',
            total_capacity_gb=mock.sentinel.size_gb,
            free_capacity_gb=mock.sentinel.free_space_gb,
            reserved_percentage=10,
            QoS_support=False)

        self._driver._update_volume_stats()
        self.assertEqual(expected_volume_stats,
                         self._driver._stats)

    def test_extend_volume(self):
        volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
        new_size_gb = 2
        expected_additional_sz_mb = 1024

        self._driver.extend_volume(volume, new_size_gb)

        self._driver._tgt_utils.extend_wt_disk.assert_called_once_with(
            volume.name, expected_additional_sz_mb)
