578 lines
20 KiB
Python
578 lines
20 KiB
Python
# 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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)
|