# Copyright 2015 Blockbridge Networks, LLC.
#
#    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.
"""
Blockbridge EPS iSCSI Volume Driver Tests
"""

import base64

try:
    from unittest import mock
except ImportError:
    import mock
from oslo_serialization import jsonutils
from oslo_utils import units
import six
from six.moves import http_client
from six.moves import urllib

from cinder import context
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
import cinder.volume.drivers.blockbridge as bb


DEFAULT_POOL_NAME = "OpenStack"
DEFAULT_POOL_QUERY = "+openstack"

FIXTURE_VOL_EXPORT_OK = """{
  "target_ip":"127.0.0.1",
  "target_port":3260,
  "target_iqn":"iqn.2009-12.com.blockbridge:t-pjxczxh-t001",
  "target_lun":0,
  "initiator_login":"mock-user-abcdef123456"
}
"""

POOL_STATS_WITHOUT_USAGE = {
    'driver_version': '1.3.0',
    'pools': [{
        'filter_function': None,
        'free_capacity_gb': 'unknown',
        'goodness_function': None,
        'location_info': 'BlockbridgeDriver:unknown:OpenStack',
        'max_over_subscription_ratio': None,
        'pool_name': 'OpenStack',
        'thin_provisioning_support': True,
        'reserved_percentage': 0,
        'total_capacity_gb': 'unknown'},
    ],
    'storage_protocol': 'iSCSI',
    'vendor_name': 'Blockbridge',
    'volume_backend_name': 'BlockbridgeISCSIDriver',
}


def common_mocks(f):
    """Decorator to set mocks common to all tests.

    The point of doing these mocks here is so that we don't accidentally set
    mocks that can't/don't get unset.
    """
    def _common_inner_inner1(inst, *args, **kwargs):
        @mock.patch("six.moves.http_client.HTTPSConnection", autospec=True)
        def _common_inner_inner2(mock_conn):
            inst.mock_httplib = mock_conn
            inst.mock_conn = mock_conn.return_value
            inst.mock_response = mock.Mock()

            inst.mock_response.read.return_value = '{}'
            inst.mock_response.status = 200

            inst.mock_conn.request.return_value = True
            inst.mock_conn.getresponse.return_value = inst.mock_response

            return f(inst, *args, **kwargs)

        return _common_inner_inner2()

    return _common_inner_inner1


class BlockbridgeISCSIDriverTestCase(test.TestCase):

    def setUp(self):
        super(BlockbridgeISCSIDriverTestCase, self).setUp()

        self.cfg = mock.Mock(spec=conf.Configuration)
        self.cfg.blockbridge_api_host = 'ut-api.blockbridge.com'
        self.cfg.blockbridge_api_port = None
        self.cfg.blockbridge_auth_scheme = 'token'
        self.cfg.blockbridge_auth_token = '0//kPIw7Ck7PUkPSKY...'
        self.cfg.blockbridge_pools = {DEFAULT_POOL_NAME: DEFAULT_POOL_QUERY}
        self.cfg.blockbridge_default_pool = None
        self.cfg.filter_function = None
        self.cfg.goodness_function = None

        def _cfg_safe_get(arg):
            return getattr(self.cfg, arg, None)

        self.cfg.safe_get.side_effect = _cfg_safe_get

        mock_exec = mock.Mock()
        mock_exec.return_value = ('', '')

        self.real_client = bb.BlockbridgeAPIClient(configuration=self.cfg)
        self.mock_client = mock.Mock(wraps=self.real_client)

        self.driver = bb.BlockbridgeISCSIDriver(execute=mock_exec,
                                                client=self.mock_client,
                                                configuration=self.cfg)

        self.user_id = '2c13bc8ef717015fda1e12e70dab24654cb6a6da'
        self.project_id = '62110b9d37f1ff3ea1f51e75812cb92ed9a08b28'

        self.volume_name = u'testvol-1'
        self.volume_id = '6546b9e9-1980-4241-a4e9-0ad9d382c032'
        self.volume_size = 1
        self.volume = dict(
            name=self.volume_name,
            size=self.volume_size,
            id=self.volume_id,
            user_id=self.user_id,
            project_id=self.project_id,
            host='fake-host')

        self.snapshot_name = u'testsnap-1'
        self.snapshot_id = '207c12af-85a7-4da6-8d39-a7457548f965'
        self.snapshot = dict(
            volume_name=self.volume_name,
            name=self.snapshot_name,
            id=self.snapshot_id,
            volume_id='55ff8a46-c35f-4ca3-9991-74e1697b220e',
            user_id=self.user_id,
            project_id=self.project_id)

        self.connector = dict(
            initiator='iqn.1994-05.com.redhat:6a528422b61')

        self.driver.do_setup(context.get_admin_context())

    @common_mocks
    def test_http_mock_success(self):
        self.mock_response.read.return_value = '{}'
        self.mock_response.status = 200

        conn = http_client.HTTPSConnection('whatever', None)
        conn.request('GET', '/blah', '{}', {})
        rsp = conn.getresponse()

        self.assertEqual('{}', rsp.read())
        self.assertEqual(200, rsp.status)

    @common_mocks
    def test_http_mock_failure(self):
        mock_body = '{"error": "no results matching query", "status": 413}'

        self.mock_response.read.return_value = mock_body
        self.mock_response.status = 413

        conn = http_client.HTTPSConnection('whatever', None)
        conn.request('GET', '/blah', '{}', {})
        rsp = conn.getresponse()

        self.assertEqual(mock_body, rsp.read())
        self.assertEqual(413, rsp.status)

    @common_mocks
    def test_cfg_api_host(self):
        with mock.patch.object(self.cfg, 'blockbridge_api_host', 'test.host'):
            self.driver.get_volume_stats(True)
        self.mock_httplib.assert_called_once_with('test.host', None)

    @common_mocks
    def test_cfg_api_port(self):
        with mock.patch.object(self.cfg, 'blockbridge_api_port', 1234):
            self.driver.get_volume_stats(True)
        self.mock_httplib.assert_called_once_with(
            self.cfg.blockbridge_api_host, 1234)

    @common_mocks
    def test_cfg_api_auth_scheme_password(self):
        self.cfg.blockbridge_auth_scheme = 'password'
        self.cfg.blockbridge_auth_user = 'mock-user'
        self.cfg.blockbridge_auth_password = 'mock-password'
        with mock.patch.object(self.driver, 'hostname', 'mock-hostname'):
            self.driver.get_volume_stats(True)

        creds = "%s:%s" % (self.cfg.blockbridge_auth_user,
                           self.cfg.blockbridge_auth_password)
        if six.PY3:
            creds = creds.encode('utf-8')
            b64_creds = base64.encodestring(creds).decode('ascii')
        else:
            b64_creds = base64.encodestring(creds)

        params = dict(
            hostname='mock-hostname',
            version=self.driver.VERSION,
            backend_name='BlockbridgeISCSIDriver',
            pool='OpenStack',
            query='+openstack')

        headers = {
            'Accept': 'application/vnd.blockbridge-3+json',
            'Authorization': "Basic %s" % b64_creds.replace("\n", ""),
            'User-Agent': "cinder-volume/%s" % self.driver.VERSION,
        }

        self.mock_conn.request.assert_called_once_with(
            'GET', mock.ANY, None, headers)
        # Parse the URL instead of comparing directly both URLs.
        # On Python 3, parameters are formatted in a random order because
        # of the hash randomization.
        conn_url = self.mock_conn.request.call_args[0][1]
        conn_params = dict(urllib.parse.parse_qsl(conn_url.split("?", 1)[1]))
        self.assertTrue(conn_url.startswith("/api/cinder/status?"),
                        repr(conn_url))
        self.assertEqual(params, conn_params)

    @common_mocks
    def test_create_volume(self):
        self.driver.create_volume(self.volume)

        url = "/volumes/%s" % self.volume_id
        create_params = dict(
            name=self.volume_name,
            query=DEFAULT_POOL_QUERY,
            capacity=self.volume_size * units.Gi)

        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

        full_url = "/api/cinder" + url
        raw_body = jsonutils.dumps(create_params)
        tsk_header = "ext_auth=keystone/%(project_id)s/%(user_id)s" % kwargs
        authz_header = "Bearer %s" % self.cfg.blockbridge_auth_token
        headers = {
            'X-Blockbridge-Task': tsk_header,
            'Accept': 'application/vnd.blockbridge-3+json',
            'Content-Type': 'application/json',
            'Authorization': authz_header,
            'User-Agent': "cinder-volume/%s" % self.driver.VERSION,
        }

        self.mock_conn.request.assert_called_once_with(
            'PUT', full_url, raw_body, headers)

    @common_mocks
    def test_create_volume_no_results(self):
        mock_body = '{"message": "no results matching query", "status": 413}'

        self.mock_response.read.return_value = mock_body
        self.mock_response.status = 413

        self.assertRaisesRegex(exception.VolumeBackendAPIException,
                               "no results matching query",
                               self.driver.create_volume,
                               self.volume)

        create_params = dict(
            name=self.volume_name,
            query=DEFAULT_POOL_QUERY,
            capacity=self.volume_size * units.Gi)

        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(
            "/volumes/%s" % self.volume_id, **kwargs)

    @common_mocks
    def test_create_volume_from_snapshot(self):
        self.driver.create_volume_from_snapshot(self.volume, self.snapshot)

        vol_src = dict(
            snapshot_id=self.snapshot_id,
            volume_id=self.snapshot['volume_id'])
        create_params = dict(
            name=self.volume_name,
            capacity=self.volume_size * units.Gi,
            src=vol_src)
        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(
            "/volumes/%s" % self.volume_id, **kwargs)

    @common_mocks
    def test_create_volume_from_snapshot_overquota(self):
        mock_body = '{"message": "over quota", "status": 413}'

        self.mock_response.read.return_value = mock_body
        self.mock_response.status = 413

        self.assertRaisesRegex(exception.VolumeBackendAPIException,
                               "over quota",
                               self.driver.create_volume_from_snapshot,
                               self.volume,
                               self.snapshot)

        vol_src = dict(
            snapshot_id=self.snapshot_id,
            volume_id=self.snapshot['volume_id'])
        create_params = dict(
            name=self.volume_name,
            capacity=self.volume_size * units.Gi,
            src=vol_src)
        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(
            "/volumes/%s" % self.volume_id, **kwargs)

    @common_mocks
    def test_create_cloned_volume(self):
        src_vref = dict(
            name='cloned_volume_source',
            size=self.volume_size,
            id='5d734467-5d77-461c-b5ac-5009dbeaa5d5',
            user_id=self.user_id,
            project_id=self.project_id)

        self.driver.create_cloned_volume(self.volume, src_vref)

        create_params = dict(
            name=self.volume_name,
            capacity=self.volume_size * units.Gi,
            src=dict(volume_id=src_vref['id']))
        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(
            "/volumes/%s" % self.volume_id, **kwargs)

    @common_mocks
    def test_create_cloned_volume_overquota(self):
        mock_body = '{"message": "over quota", "status": 413}'

        self.mock_response.read.return_value = mock_body
        self.mock_response.status = 413

        src_vref = dict(
            name='cloned_volume_source',
            size=self.volume_size,
            id='5d734467-5d77-461c-b5ac-5009dbeaa5d5',
            user_id=self.user_id,
            project_id=self.project_id)

        self.assertRaisesRegex(exception.VolumeBackendAPIException,
                               "over quota",
                               self.driver.create_cloned_volume,
                               self.volume,
                               src_vref)

        create_params = dict(
            name=self.volume_name,
            capacity=self.volume_size * units.Gi,
            src=dict(volume_id=src_vref['id']))
        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(
            "/volumes/%s" % self.volume_id, **kwargs)

    @common_mocks
    def test_extend_volume(self):
        self.driver.extend_volume(self.volume, 2)

        url = "/volumes/%s" % self.volume_id
        kwargs = dict(
            action='grow',
            method='POST',
            params=dict(capacity=(2 * units.Gi)),
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_extend_volume_overquota(self):
        mock_body = '{"message": "over quota", "status": 413}'
        self.mock_response.read.return_value = mock_body
        self.mock_response.status = 413

        self.assertRaisesRegex(exception.VolumeBackendAPIException,
                               "over quota",
                               self.driver.extend_volume,
                               self.volume,
                               2)

        url = "/volumes/%s" % self.volume_id
        kwargs = dict(
            action='grow',
            method='POST',
            params=dict(capacity=(2 * units.Gi)),
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_delete_volume(self):
        self.driver.delete_volume(self.volume)

        url = "/volumes/%s" % self.volume_id
        kwargs = dict(
            method='DELETE',
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_create_snapshot(self):
        self.driver.create_snapshot(self.snapshot)

        url = "/volumes/%s/snapshots/%s" % (self.snapshot['volume_id'],
                                            self.snapshot['id'])
        create_params = dict(
            name=self.snapshot_name)
        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_create_snapshot_overquota(self):
        mock_body = '{"message": "over quota", "status": 413}'
        self.mock_response.read.return_value = mock_body
        self.mock_response.status = 413

        self.assertRaisesRegex(exception.VolumeBackendAPIException,
                               "over quota",
                               self.driver.create_snapshot,
                               self.snapshot)

        url = "/volumes/%s/snapshots/%s" % (self.snapshot['volume_id'],
                                            self.snapshot['id'])
        create_params = dict(
            name=self.snapshot_name)
        kwargs = dict(
            method='PUT',
            params=create_params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_delete_snapshot(self):
        self.driver.delete_snapshot(self.snapshot)

        url = "/volumes/%s/snapshots/%s" % (self.snapshot['volume_id'],
                                            self.snapshot['id'])
        kwargs = dict(
            method='DELETE',
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    @mock.patch('cinder.volume.utils.generate_username')
    @mock.patch('cinder.volume.utils.generate_password')
    def test_initialize_connection(self,
                                   mock_generate_password,
                                   mock_generate_username):
        mock_generate_username.return_value = 'mock-user-abcdef123456'
        mock_generate_password.return_value = 'mock-password-abcdef123456'

        self.mock_response.read.return_value = FIXTURE_VOL_EXPORT_OK
        self.mock_response.status = 200

        props = self.driver.initialize_connection(self.volume, self.connector)

        expected_props = dict(
            driver_volume_type="iscsi",
            data=dict(
                auth_method="CHAP",
                auth_username='mock-user-abcdef123456',
                auth_password='mock-password-abcdef123456',
                target_discovered=False,
                target_iqn="iqn.2009-12.com.blockbridge:t-pjxczxh-t001",
                target_lun=0,
                target_portal="127.0.0.1:3260",
                volume_id=self.volume_id))

        self.assertEqual(expected_props, props)

        ini_name = urllib.parse.quote(self.connector["initiator"], "")
        url = "/volumes/%s/exports/%s" % (self.volume_id, ini_name)
        params = dict(
            chap_user="mock-user-abcdef123456",
            chap_secret="mock-password-abcdef123456")
        kwargs = dict(
            method='PUT',
            params=params,
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_terminate_connection(self):
        self.driver.terminate_connection(self.volume, self.connector)

        ini_name = urllib.parse.quote(self.connector["initiator"], "")
        url = "/volumes/%s/exports/%s" % (self.volume_id, ini_name)
        kwargs = dict(
            method='DELETE',
            user_id=self.user_id,
            project_id=self.project_id)

        self.mock_client.submit.assert_called_once_with(url, **kwargs)

    @common_mocks
    def test_get_volume_stats_without_usage(self):
        with mock.patch.object(self.driver, 'hostname', 'mock-hostname'):
            self.driver.get_volume_stats(True)

        p = {
            'query': '+openstack',
            'pool': 'OpenStack',
            'hostname': 'mock-hostname',
            'version': '1.3.0',
            'backend_name': 'BlockbridgeISCSIDriver',
        }

        self.mock_client.submit.assert_called_once_with('/status', params=p)
        self.assertEqual(POOL_STATS_WITHOUT_USAGE, self.driver._stats)

    @common_mocks
    def test_get_volume_stats_forbidden(self):
        self.mock_response.status = 403
        self.assertRaisesRegex(exception.NotAuthorized,
                               "Insufficient privileges",
                               self.driver.get_volume_stats,
                               True)

    @common_mocks
    def test_get_volume_stats_unauthorized(self):
        self.mock_response.status = 401
        self.assertRaisesRegex(exception.NotAuthorized,
                               "Invalid credentials",
                               self.driver.get_volume_stats,
                               True)
