cinder/cinder/tests/unit/test_blockbridge.py

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)