#    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 os
import sys
import time
from unittest import mock

from oslo_concurrency import processutils as putils

from cinder import context
from cinder import exception
from cinder.tests.unit.targets import targets_fixture as tf
from cinder.tests.unit import test
from cinder import utils
from cinder.volume.targets import tgt
from cinder.volume import volume_utils


class TestTgtAdmDriver(tf.TargetDriverFixture):

    def setUp(self):
        super(TestTgtAdmDriver, self).setUp()
        self.configuration.get = mock.Mock(side_effect=self.fake_get)

        self.target = tgt.TgtAdm(root_helper=utils.get_root_helper(),
                                 configuration=self.configuration)
        self.testvol_path = \
            '/dev/stack-volumes-lvmdriver-1/%s' % self.VOLUME_NAME

        self.fake_iscsi_scan =\
            ('Target 1: %(test_vol)s\n'
             '    System information:\n'
             '        Driver: iscsi\n'
             '        State: ready\n'
             '    I_T nexus information:\n'
             '    LUN information:\n'
             '        LUN: 0\n'
             '            Type: controller\n'
             '            SCSI ID: IET     00010000\n'
             '            SCSI SN: beaf10\n'
             '            Size: 0 MB, Block size: 1\n'
             '            Online: Yes\n'
             '            Removable media: No\n'
             '            Prevent removal: No\n'
             '            Readonly: No\n'
             '            SWP: No\n'
             '            Thin-provisioning: No\n'
             '            Backing store type: null\n'
             '            Backing store path: None\n'
             '            Backing store flags:\n'
             '        LUN: 1\n'
             '            Type: disk\n'
             '            SCSI ID: IET     00010001\n'
             '            SCSI SN: beaf11\n'
             '            Size: 1074 MB, Block size: 512\n'
             '            Online: Yes\n'
             '            Removable media: No\n'
             '            Prevent removal: No\n'
             '            Readonly: No\n'
             '            SWP: No\n'
             '            Thin-provisioning: No\n'
             '            Backing store type: rdwr\n'
             '            Backing store path: %(bspath)s\n'
             '            Backing store flags:\n'
             '    Account information:\n'
             '        mDVpzk8cZesdahJC9h73\n'
             '    ACL information:\n'
             '        ALL"\n' % {'test_vol': self.test_vol,
                                 'bspath': self.testvol_path})
        self.patch('time.sleep')

    def fake_get(self, value, default):
        if value in ('iscsi_target_flags', 'iscsi_write_cache'):
            return getattr(self, value, default)

    def test_iscsi_protocol(self):
        self.assertEqual('iscsi', self.target.iscsi_protocol)

    def test_get_target(self):
        with mock.patch('cinder.privsep.targets.tgt.tgtadmin_show',
                        return_value=(self.fake_iscsi_scan, None)):
            iqn = self.test_vol
            self.assertEqual('1', self.target._get_target(iqn))

    def test_verify_backing_lun(self):
        iqn = self.test_vol

        with mock.patch('cinder.privsep.targets.tgt.tgtadmin_show',
                        return_value=(self.fake_iscsi_scan, None)):
            self.assertTrue(self.target._verify_backing_lun(iqn, '1'))

        # Test the failure case
        bad_scan = self.fake_iscsi_scan.replace('LUN: 1', 'LUN: 3')

        with mock.patch('cinder.privsep.targets.tgt.tgtadmin_show',
                        return_value=(bad_scan, None)):
            self.assertFalse(self.target._verify_backing_lun(iqn, '1'))

    @mock.patch.object(time, 'sleep')
    @mock.patch('cinder.privsep.targets.tgt.tgtadm_create')
    def test_recreate_backing_lun(self, mock_privsep, mock_sleep):
        mock_privsep.return_value = ('out', 'err')
        self.target._recreate_backing_lun(self.test_vol, '1',
                                          self.testvol['name'],
                                          self.testvol_path)

        mock_privsep.assert_called_once_with('1', self.testvol_path)

        # Test the failure case
        mock_privsep.side_effect = putils.ProcessExecutionError
        self.assertIsNone(
            self.target._recreate_backing_lun(self.test_vol,
                                              '1',
                                              self.testvol['name'],
                                              self.testvol_path))

    def test_get_iscsi_target(self):
        ctxt = context.get_admin_context()
        expected = 0
        self.assertEqual(expected,
                         self.target._get_iscsi_target(ctxt,
                                                       self.testvol['id']))

    def test_get_target_and_lun(self):
        lun = 1
        iscsi_target = 0
        ctxt = context.get_admin_context()
        expected = (iscsi_target, lun)
        self.assertEqual(expected,
                         self.target._get_target_and_lun(ctxt, self.testvol))

    @test.testtools.skipIf(sys.platform == "darwin", "SKIP on OSX")
    def test_create_iscsi_target(self):
        with mock.patch('cinder.privsep.targets.tgt.tgtadm_show',
                        return_value=('', '')), \
                mock.patch.object(self.target, '_get_target',
                                  side_effect=lambda x: 1), \
                mock.patch('cinder.privsep.targets.tgt.tgtadmin_update',
                           return_value=('', '')), \
                mock.patch.object(self.target, '_verify_backing_lun',
                                  side_effect=lambda x, y: True):
            self.assertEqual(
                1,
                self.target.create_iscsi_target(
                    self.test_vol,
                    1,
                    0,
                    self.fake_volumes_dir))

    @test.testtools.skipIf(sys.platform == "darwin", "SKIP on OSX")
    def test_create_iscsi_target_content(self):

        self.iscsi_target_flags = 'foo'
        self.iscsi_write_cache = 'bar'

        mock_open = mock.mock_open()
        with mock.patch('cinder.privsep.targets.tgt.tgtadm_show',
                        return_value=('', '')), \
                mock.patch.object(self.target, '_get_target',
                                  side_effect=lambda x: 1), \
                mock.patch.object(self.target, '_verify_backing_lun',
                                  side_effect=lambda x, y: True), \
                mock.patch('cinder.privsep.targets.tgt.tgtadmin_update',
                           return_value=('', '')), \
                mock.patch('cinder.volume.targets.tgt.open',
                           mock_open, create=True):
            self.assertEqual(
                1,
                self.target.create_iscsi_target(
                    self.test_vol,
                    1,
                    0,
                    self.testvol_path,
                    chap_auth=('chap_foo', 'chap_bar')))

    @test.testtools.skipIf(sys.platform == "darwin", "SKIP on OSX")
    def test_create_iscsi_target_already_exists(self):
        def _fake_execute(*args, **kwargs):
            if 'update' in args:
                raise putils.ProcessExecutionError(
                    exit_code=1,
                    stdout='',
                    stderr='target already exists',
                    cmd='tgtad --lld iscsi --op show --mode target')
            else:
                return 'fake out', 'fake err'

        with mock.patch.object(self.target, '_get_target',
                               side_effect=lambda x: 1), \
                mock.patch.object(self.target, '_verify_backing_lun',
                                  side_effect=lambda x, y: True), \
                mock.patch('cinder.privsep.targets.tgt.tgtadmin_update',
                           return_value=('', '')), \
                mock.patch('cinder.privsep.targets.tgt.tgtadm_show',
                           _fake_execute):
            self.assertEqual(
                1,
                self.target.create_iscsi_target(
                    self.test_vol,
                    1,
                    0,
                    self.fake_volumes_dir))

    @mock.patch('os.path.isfile', return_value=True)
    @mock.patch('os.path.exists', return_value=True)
    @mock.patch('cinder.privsep.targets.tgt.tgtadmin_delete')
    @mock.patch('os.unlink', return_value=None)
    def test_delete_target_not_found(self,
                                     mock_unlink,
                                     mock_exec,
                                     mock_pathexists,
                                     mock_isfile):
        def _fake_execute(*args, **kwargs):
            raise putils.ProcessExecutionError(
                exit_code=1,
                stdout='',
                stderr='can\'t find the target',
                cmd='tgt-admin --force --delete')

        def _fake_execute_wrong_message(*args, **kwargs):
            raise putils.ProcessExecutionError(
                exit_code=1,
                stdout='',
                stderr='this is not the error you are looking for',
                cmd='tgt-admin --force --delete')

        mock_exec.side_effect = _fake_execute

        with mock.patch.object(self.target, '_get_target', return_value=False):
            self.assertIsNone(self.target.remove_iscsi_target(
                1,
                0,
                self.VOLUME_ID,
                self.VOLUME_NAME))

            mock_exec.side_effect = _fake_execute_wrong_message
            self.assertRaises(exception.ISCSITargetRemoveFailed,
                              self.target.remove_iscsi_target,
                              1,
                              0,
                              self.VOLUME_ID,
                              self.VOLUME_NAME)

    @mock.patch('os.path.isfile', return_value=True)
    @mock.patch('os.path.exists', return_value=True)
    @mock.patch('cinder.privsep.targets.tgt.tgtadmin_delete')
    @mock.patch('os.unlink', return_value=None)
    def test_delete_target_acl_not_found(self,
                                         mock_unlink,
                                         mock_exec,
                                         mock_pathexists,
                                         mock_isfile):
        def _fake_execute(*args, **kwargs):
            raise putils.ProcessExecutionError(
                exit_code=1,
                stdout='',
                stderr='this access control rule does not exist',
                cmd='tgt-admin --force --delete')

        def _fake_execute_wrong_message(*args, **kwargs):
            raise putils.ProcessExecutionError(
                exit_code=1,
                stdout='',
                stderr='this is not the error you are looking for',
                cmd='tgt-admin --force --delete')

        mock_exec.side_effect = _fake_execute

        with mock.patch.object(self.target, '_get_target', return_value=False):
            self.assertIsNone(self.target.remove_iscsi_target(
                1,
                0,
                self.VOLUME_ID,
                self.VOLUME_NAME))

            mock_exec.side_effect = _fake_execute_wrong_message
            self.assertRaises(exception.ISCSITargetRemoveFailed,
                              self.target.remove_iscsi_target,
                              1,
                              0,
                              self.VOLUME_ID,
                              self.VOLUME_NAME)

    @mock.patch.object(tgt.TgtAdm, '_get_iscsi_properties')
    def test_initialize_connection(self, mock_get_iscsi):

        connector = {'initiator': 'fake_init'}

        # Test the normal case
        mock_get_iscsi.return_value = 'foo bar'
        expected_return = {'driver_volume_type': 'iscsi',
                           'data': 'foo bar'}
        self.assertEqual(expected_return,
                         self.target.initialize_connection(self.testvol,
                                                           connector))

    @mock.patch('cinder.utils.execute')
    @mock.patch.object(tgt.TgtAdm, '_get_target')
    @mock.patch.object(os.path, 'exists')
    @mock.patch.object(os.path, 'isfile')
    @mock.patch.object(os, 'unlink')
    @mock.patch('cinder.privsep.targets.tgt.tgtadmin_delete')
    def test_remove_iscsi_target(self,
                                 mock_delete,
                                 mock_unlink,
                                 mock_isfile,
                                 mock_path_exists,
                                 mock_get_target,
                                 mock_execute):

        # Test the failure case: path does not exist
        mock_path_exists.return_value = None
        self.assertIsNone(self.target.remove_iscsi_target(
            0,
            1,
                          self.testvol['id'],
                          self.testvol['name']))

        # Test the normal case
        mock_path_exists.return_value = True
        mock_isfile.return_value = True
        self.target.remove_iscsi_target(0,
                                        1,
                                        self.testvol['id'],
                                        self.testvol['name'])
        mock_delete.assert_called_with(
            self.iscsi_target_prefix + self.testvol['name'])

    @test.testtools.skipIf(sys.platform == "darwin", "SKIP on OSX")
    def test_create_export(self):
        expected_result = {'location': '10.9.8.7:3260,1 ' +
                           self.iscsi_target_prefix +
                           self.testvol['name'] + ' 1',
                           'auth': 'CHAP QZJb P68e'}

        with mock.patch('cinder.privsep.targets.tgt.tgtadm_show',
                        return_value=('', '')), \
                mock.patch.object(self.target, '_get_target',
                                  side_effect=lambda x: 1), \
                mock.patch.object(self.target, '_verify_backing_lun',
                                  side_effect=lambda x, y: True), \
                mock.patch.object(self.target, '_get_target_chap_auth',
                                  side_effect=lambda x, y: None) as m_chap, \
                mock.patch.object(volume_utils, 'generate_username',
                                  side_effect=lambda: 'QZJb'), \
                mock.patch('cinder.privsep.targets.tgt.tgtadmin_update',
                           return_value=('', '')), \
                mock.patch.object(volume_utils, 'generate_password',
                                  side_effect=lambda: 'P68e'):

            ctxt = context.get_admin_context()
            self.assertEqual(expected_result,
                             self.target.create_export(ctxt,
                                                       self.testvol,
                                                       self.fake_volumes_dir))

            m_chap.side_effect = lambda x, y: ('otzL', '234Z')

            expected_result['auth'] = ('CHAP otzL 234Z')

            self.assertEqual(expected_result,
                             self.target.create_export(ctxt,
                                                       self.testvol,
                                                       self.fake_volumes_dir))

    @mock.patch.object(tgt.TgtAdm, '_get_target_chap_auth')
    @mock.patch.object(tgt.TgtAdm, 'create_iscsi_target')
    def test_ensure_export(self, _mock_create, mock_get_chap):
        ctxt = context.get_admin_context()
        mock_get_chap.return_value = ('foo', 'bar')
        self.target.ensure_export(ctxt,
                                  self.testvol,
                                  self.fake_volumes_dir)

        _mock_create.assert_called_once_with(
            self.iscsi_target_prefix + self.testvol['name'],
            0, 1, self.fake_volumes_dir, ('foo', 'bar'),
            check_exit_code=False,
            old_name=None,
            portals_ips=[self.configuration.target_ip_address],
            portals_port=self.configuration.target_port)

    @test.testtools.skipIf(sys.platform == "darwin", "SKIP on OSX")
    def test_create_iscsi_target_retry(self):
        with mock.patch('cinder.privsep.targets.tgt.tgtadm_show',
                        return_value=('', '')), \
                mock.patch.object(self.target, '_get_target',
                                  side_effect=[None, None, 1]) as get_target, \
                mock.patch('cinder.privsep.targets.tgt.tgtadmin_update',
                           return_value=('', '')), \
                mock.patch.object(self.target, '_verify_backing_lun',
                                  side_effect=lambda x, y: True):
            self.assertEqual(
                1,
                self.target.create_iscsi_target(
                    self.test_vol,
                    1,
                    0,
                    self.fake_volumes_dir))
            # 3 - default retries count value for utils.retry
            self.assertEqual(3, get_target.call_count)
