Zadara VPSA: Move to API access key authentication
Add 'access_key' API authentication login method given as input and move access_key to HTTP request header. Fix unittest accordingly. Change-Id: I11df21a7a213a0700d277d57ceee40f636955c50
This commit is contained in:
parent
6e585da2ff
commit
3ebefda515
@ -26,6 +26,16 @@ from cinder.volume import configuration as conf
|
|||||||
from cinder.volume.drivers import zadara
|
from cinder.volume.drivers import zadara
|
||||||
|
|
||||||
|
|
||||||
|
def check_access_key(func):
|
||||||
|
"""A decorator for all operations that needed an API before executing"""
|
||||||
|
def wrap(self, *args, **kwargs):
|
||||||
|
if not self._is_correct_access_key():
|
||||||
|
return RUNTIME_VARS['bad_login']
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_RUNTIME_VARS = {
|
DEFAULT_RUNTIME_VARS = {
|
||||||
'status': 200,
|
'status': 200,
|
||||||
'user': 'test',
|
'user': 'test',
|
||||||
@ -83,12 +93,20 @@ RUNTIME_VARS = None
|
|||||||
|
|
||||||
|
|
||||||
class FakeResponse(object):
|
class FakeResponse(object):
|
||||||
def __init__(self, method, url, body):
|
def __init__(self, method, url, params, body, headers, **kwargs):
|
||||||
|
# kwargs include: verify, timeout
|
||||||
self.method = method
|
self.method = method
|
||||||
self.url = url
|
self.url = url
|
||||||
self.body = body
|
self.body = body
|
||||||
|
self.params = params
|
||||||
|
self.headers = headers
|
||||||
self.status = RUNTIME_VARS['status']
|
self.status = RUNTIME_VARS['status']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_key(self):
|
||||||
|
"""Returns Response Access Key"""
|
||||||
|
return self.headers["X-Access-Key"]
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
ops = {'POST': [('/api/users/login.xml', self._login),
|
ops = {'POST': [('/api/users/login.xml', self._login),
|
||||||
('/api/volumes.xml', self._create_volume),
|
('/api/volumes.xml', self._create_volume),
|
||||||
@ -113,13 +131,13 @@ class FakeResponse(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ops_list = ops[self.method]
|
ops_list = ops[self.method]
|
||||||
modified_url = self.url.split('?')[0]
|
|
||||||
for (templ_url, func) in ops_list:
|
for (templ_url, func) in ops_list:
|
||||||
if self._compare_url(modified_url, templ_url):
|
if self._compare_url(self.url, templ_url):
|
||||||
result = func()
|
result = func()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _compare_url(self, url, template_url):
|
@staticmethod
|
||||||
|
def _compare_url(url, template_url):
|
||||||
items = url.split('/')
|
items = url.split('/')
|
||||||
titems = template_url.split('/')
|
titems = template_url.split('/')
|
||||||
for (i, titem) in enumerate(titems):
|
for (i, titem) in enumerate(titems):
|
||||||
@ -127,36 +145,26 @@ class FakeResponse(object):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _get_parameters(self, data):
|
@staticmethod
|
||||||
items = data.split('&')
|
def _get_counter():
|
||||||
params = {}
|
|
||||||
for item in items:
|
|
||||||
if item:
|
|
||||||
(k, v) = item.split('=')
|
|
||||||
params[k] = v
|
|
||||||
return params
|
|
||||||
|
|
||||||
def _get_counter(self):
|
|
||||||
cnt = RUNTIME_VARS['counter']
|
cnt = RUNTIME_VARS['counter']
|
||||||
RUNTIME_VARS['counter'] += 1
|
RUNTIME_VARS['counter'] += 1
|
||||||
return cnt
|
return cnt
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if (params['user'] == RUNTIME_VARS['user'] and
|
if (params['user'] == RUNTIME_VARS['user'] and
|
||||||
params['password'] == RUNTIME_VARS['password']):
|
params['password'] == RUNTIME_VARS['password']):
|
||||||
return RUNTIME_VARS['login'] % RUNTIME_VARS['access_key']
|
return RUNTIME_VARS['login'] % RUNTIME_VARS['access_key']
|
||||||
else:
|
else:
|
||||||
return RUNTIME_VARS['bad_login']
|
return RUNTIME_VARS['bad_login']
|
||||||
|
|
||||||
def _incorrect_access_key(self, params):
|
def _is_correct_access_key(self):
|
||||||
return (params['access_key'] != RUNTIME_VARS['access_key'])
|
return self.access_key == RUNTIME_VARS['access_key']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _create_volume(self):
|
def _create_volume(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
params['display-name'] = params['name']
|
params['display-name'] = params['name']
|
||||||
params['cg-name'] = params['name']
|
params['cg-name'] = params['name']
|
||||||
params['snapshots'] = []
|
params['snapshots'] = []
|
||||||
@ -165,22 +173,21 @@ class FakeResponse(object):
|
|||||||
RUNTIME_VARS['volumes'].append((vpsa_vol, params))
|
RUNTIME_VARS['volumes'].append((vpsa_vol, params))
|
||||||
return RUNTIME_VARS['good']
|
return RUNTIME_VARS['good']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _create_server(self):
|
def _create_server(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
params['display-name'] = params['display_name']
|
params['display-name'] = params['display_name']
|
||||||
vpsa_srv = 'srv-%07d' % self._get_counter()
|
vpsa_srv = 'srv-%07d' % self._get_counter()
|
||||||
RUNTIME_VARS['servers'].append((vpsa_srv, params))
|
RUNTIME_VARS['servers'].append((vpsa_srv, params))
|
||||||
return RUNTIME_VARS['server_created'] % vpsa_srv
|
return RUNTIME_VARS['server_created'] % vpsa_srv
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _attach(self):
|
def _attach(self):
|
||||||
params = self._get_parameters(self.body)
|
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
srv = self.url.split('/')[3]
|
srv = self.url.split('/')[3]
|
||||||
|
|
||||||
|
params = self.body
|
||||||
|
|
||||||
vol = params['volume_name[]']
|
vol = params['volume_name[]']
|
||||||
|
|
||||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||||
@ -195,11 +202,9 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
return RUNTIME_VARS['bad_volume']
|
return RUNTIME_VARS['bad_volume']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _detach(self):
|
def _detach(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
vol = self.url.split('/')[3]
|
vol = self.url.split('/')[3]
|
||||||
srv = params['server_name[]']
|
srv = params['server_name[]']
|
||||||
|
|
||||||
@ -214,11 +219,9 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
return RUNTIME_VARS['bad_volume']
|
return RUNTIME_VARS['bad_volume']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _expand(self):
|
def _expand(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
vol = self.url.split('/')[3]
|
vol = self.url.split('/')[3]
|
||||||
capacity = params['capacity']
|
capacity = params['capacity']
|
||||||
|
|
||||||
@ -229,11 +232,9 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
return RUNTIME_VARS['bad_volume']
|
return RUNTIME_VARS['bad_volume']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _create_snapshot(self):
|
def _create_snapshot(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
cg_name = self.url.split('/')[3]
|
cg_name = self.url.split('/')[3]
|
||||||
snap_name = params['display_name']
|
snap_name = params['display_name']
|
||||||
|
|
||||||
@ -249,6 +250,7 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
return RUNTIME_VARS['bad_volume']
|
return RUNTIME_VARS['bad_volume']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _delete_snapshot(self):
|
def _delete_snapshot(self):
|
||||||
snap = self.url.split('/')[3].split('.')[0]
|
snap = self.url.split('/')[3].split('.')[0]
|
||||||
|
|
||||||
@ -259,11 +261,9 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
return RUNTIME_VARS['bad_volume']
|
return RUNTIME_VARS['bad_volume']
|
||||||
|
|
||||||
|
@check_access_key
|
||||||
def _create_clone(self):
|
def _create_clone(self):
|
||||||
params = self._get_parameters(self.body)
|
params = self.body
|
||||||
if self._incorrect_access_key(params):
|
|
||||||
return RUNTIME_VARS['bad_login']
|
|
||||||
|
|
||||||
params['display-name'] = params['name']
|
params['display-name'] = params['name']
|
||||||
params['cg-name'] = params['name']
|
params['cg-name'] = params['name']
|
||||||
params['capacity'] = 1
|
params['capacity'] = 1
|
||||||
@ -439,16 +439,24 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
class FakeRequests(object):
|
class FakeRequests(object):
|
||||||
"""A fake requests for zadara volume driver tests."""
|
"""A fake requests for zadara volume driver tests."""
|
||||||
def __init__(self, method, api_url, data, verify):
|
def __init__(self, method, api_url, params=None, data=None,
|
||||||
|
headers=None, **kwargs):
|
||||||
url = parse.urlparse(api_url).path
|
url = parse.urlparse(api_url).path
|
||||||
res = FakeResponse(method, url, data)
|
res = FakeResponse(method, url, params, data, headers, **kwargs)
|
||||||
self.content = res.read()
|
self.content = res.read()
|
||||||
self.status_code = res.status
|
self.status_code = res.status
|
||||||
|
|
||||||
|
|
||||||
class ZadaraVPSADriverTestCase(test.TestCase):
|
class ZadaraVPSADriverTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ZadaraVPSADriverTestCase, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.configuration = None
|
||||||
|
self.driver = None
|
||||||
|
|
||||||
"""Test case for Zadara VPSA volume driver."""
|
"""Test case for Zadara VPSA volume driver."""
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ZadaraVPSADriverTestCase, self).setUp()
|
super(ZadaraVPSADriverTestCase, self).setUp()
|
||||||
|
|
||||||
@ -462,6 +470,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.configuration.zadara_vpsa_port = '80'
|
self.configuration.zadara_vpsa_port = '80'
|
||||||
self.configuration.zadara_user = 'test'
|
self.configuration.zadara_user = 'test'
|
||||||
self.configuration.zadara_password = 'test_password'
|
self.configuration.zadara_password = 'test_password'
|
||||||
|
self.configuration.zadara_access_key = '0123456789ABCDEF'
|
||||||
self.configuration.zadara_vpsa_poolname = 'pool-0001'
|
self.configuration.zadara_vpsa_poolname = 'pool-0001'
|
||||||
self.configuration.zadara_vol_encrypt = False
|
self.configuration.zadara_vol_encrypt = False
|
||||||
self.configuration.zadara_vol_name_template = 'OS_%s'
|
self.configuration.zadara_vol_name_template = 'OS_%s'
|
||||||
@ -472,14 +481,14 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
configuration=self.configuration))
|
configuration=self.configuration))
|
||||||
self.driver.do_setup(None)
|
self.driver.do_setup(None)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_create_destroy(self):
|
def test_create_destroy(self):
|
||||||
"""Create/Delete volume."""
|
"""Create/Delete volume."""
|
||||||
volume = {'name': 'test_volume_01', 'size': 1}
|
volume = {'name': 'test_volume_01', 'size': 1}
|
||||||
self.driver.create_volume(volume)
|
self.driver.create_volume(volume)
|
||||||
self.driver.delete_volume(volume)
|
self.driver.delete_volume(volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_create_destroy_multiple(self):
|
def test_create_destroy_multiple(self):
|
||||||
"""Create/Delete multiple volumes."""
|
"""Create/Delete multiple volumes."""
|
||||||
self.driver.create_volume({'name': 'test_volume_01', 'size': 1})
|
self.driver.create_volume({'name': 'test_volume_01', 'size': 1})
|
||||||
@ -490,13 +499,13 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.delete_volume({'name': 'test_volume_01'})
|
self.driver.delete_volume({'name': 'test_volume_01'})
|
||||||
self.driver.delete_volume({'name': 'test_volume_04'})
|
self.driver.delete_volume({'name': 'test_volume_04'})
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_destroy_non_existent(self):
|
def test_destroy_non_existent(self):
|
||||||
"""Delete non-existent volume."""
|
"""Delete non-existent volume."""
|
||||||
volume = {'name': 'test_volume_02', 'size': 1}
|
volume = {'name': 'test_volume_02', 'size': 1}
|
||||||
self.driver.delete_volume(volume)
|
self.driver.delete_volume(volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_empty_apis(self):
|
def test_empty_apis(self):
|
||||||
"""Test empty func (for coverage only)."""
|
"""Test empty func (for coverage only)."""
|
||||||
context = None
|
context = None
|
||||||
@ -509,7 +518,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
None)
|
None)
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_volume_attach_detach(self):
|
def test_volume_attach_detach(self):
|
||||||
"""Test volume attachment and detach."""
|
"""Test volume attachment and detach."""
|
||||||
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
||||||
@ -529,25 +538,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.terminate_connection(volume, connector)
|
self.driver.terminate_connection(volume, connector)
|
||||||
self.driver.delete_volume(volume)
|
self.driver.delete_volume(volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_volume_attach_multiple_detach(self):
|
|
||||||
"""Test multiple volume attachment and detach."""
|
|
||||||
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
|
||||||
connector1 = dict(initiator='test_iqn.1')
|
|
||||||
connector2 = dict(initiator='test_iqn.2')
|
|
||||||
connector3 = dict(initiator='test_iqn.3')
|
|
||||||
|
|
||||||
self.driver.create_volume(volume)
|
|
||||||
self.driver.initialize_connection(volume, connector1)
|
|
||||||
self.driver.initialize_connection(volume, connector2)
|
|
||||||
self.driver.initialize_connection(volume, connector3)
|
|
||||||
|
|
||||||
self.driver.terminate_connection(volume, connector1)
|
|
||||||
self.driver.terminate_connection(volume, connector3)
|
|
||||||
self.driver.terminate_connection(volume, connector2)
|
|
||||||
self.driver.delete_volume(volume)
|
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
|
||||||
def test_wrong_attach_params(self):
|
def test_wrong_attach_params(self):
|
||||||
"""Test different wrong attach scenarios."""
|
"""Test different wrong attach scenarios."""
|
||||||
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
||||||
@ -556,32 +547,49 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.initialize_connection,
|
self.driver.initialize_connection,
|
||||||
volume1, connector1)
|
volume1, connector1)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_wrong_detach_params(self):
|
def test_wrong_detach_params(self):
|
||||||
"""Test different wrong detachment scenarios."""
|
"""Test different wrong detachment scenarios."""
|
||||||
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
||||||
|
# Volume is not created.
|
||||||
|
self.assertRaises(exception.VolumeNotFound,
|
||||||
|
self.driver.terminate_connection,
|
||||||
|
volume1, None)
|
||||||
|
|
||||||
|
self.driver.create_volume(volume1)
|
||||||
|
connector1 = dict(initiator='test_iqn.1')
|
||||||
|
# Server is not found. Volume is found
|
||||||
|
self.assertRaises(zadara.ZadaraServerNotFound,
|
||||||
|
self.driver.terminate_connection,
|
||||||
|
volume1, connector1)
|
||||||
|
|
||||||
volume2 = {'name': 'test_volume_02', 'size': 1, 'id': 102}
|
volume2 = {'name': 'test_volume_02', 'size': 1, 'id': 102}
|
||||||
volume3 = {'name': 'test_volume_03', 'size': 1, 'id': 103}
|
volume3 = {'name': 'test_volume_03', 'size': 1, 'id': 103}
|
||||||
connector1 = dict(initiator='test_iqn.1')
|
|
||||||
connector2 = dict(initiator='test_iqn.2')
|
connector2 = dict(initiator='test_iqn.2')
|
||||||
connector3 = dict(initiator='test_iqn.3')
|
connector3 = dict(initiator='test_iqn.3')
|
||||||
self.driver.create_volume(volume1)
|
|
||||||
self.driver.create_volume(volume2)
|
self.driver.create_volume(volume2)
|
||||||
self.driver.initialize_connection(volume1, connector1)
|
self.driver.initialize_connection(volume1, connector1)
|
||||||
self.driver.initialize_connection(volume2, connector2)
|
self.driver.initialize_connection(volume2, connector2)
|
||||||
|
# volume is found. Server not found
|
||||||
self.assertRaises(zadara.ZadaraServerNotFound,
|
self.assertRaises(zadara.ZadaraServerNotFound,
|
||||||
self.driver.terminate_connection,
|
self.driver.terminate_connection,
|
||||||
volume1, connector3)
|
volume1, connector3)
|
||||||
|
# Server is found. volume not found
|
||||||
self.assertRaises(exception.VolumeNotFound,
|
self.assertRaises(exception.VolumeNotFound,
|
||||||
self.driver.terminate_connection,
|
self.driver.terminate_connection,
|
||||||
volume3, connector1)
|
volume3, connector1)
|
||||||
|
# Server and volume exits but not attached
|
||||||
self.assertRaises(exception.FailedCmdWithDump,
|
self.assertRaises(exception.FailedCmdWithDump,
|
||||||
self.driver.terminate_connection,
|
self.driver.terminate_connection,
|
||||||
volume1, connector2)
|
volume1, connector2)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
self.driver.terminate_connection(volume1, connector1)
|
||||||
|
self.driver.terminate_connection(volume2, connector2)
|
||||||
|
|
||||||
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_wrong_login_reply(self):
|
def test_wrong_login_reply(self):
|
||||||
"""Test wrong login reply."""
|
"""Test wrong login reply."""
|
||||||
|
self.configuration.zadara_access_key = None
|
||||||
|
|
||||||
RUNTIME_VARS['login'] = """<hash>
|
RUNTIME_VARS['login'] = """<hash>
|
||||||
<access-key>%s</access-key>
|
<access-key>%s</access-key>
|
||||||
@ -605,16 +613,18 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.assertRaises(exception.MalformedResponse,
|
self.assertRaises(exception.MalformedResponse,
|
||||||
self.driver.do_setup, None)
|
self.driver.do_setup, None)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request')
|
@mock.patch.object(requests.Session, 'request')
|
||||||
def test_ssl_use(self, request):
|
def test_ssl_use(self, request):
|
||||||
"""Coverage test for SSL connection."""
|
"""Coverage test for SSL connection."""
|
||||||
self.configuration.zadara_ssl_cert_verify = True
|
self.configuration.zadara_ssl_cert_verify = True
|
||||||
self.configuration.zadara_vpsa_use_ssl = True
|
self.configuration.zadara_vpsa_use_ssl = True
|
||||||
self.configuration.driver_ssl_cert_path = '/path/to/cert'
|
self.configuration.driver_ssl_cert_path = '/path/to/cert'
|
||||||
|
|
||||||
|
fake_request_ctrls = FakeRequests("GET", "/api/vcontrollers.xml")
|
||||||
|
raw_controllers = fake_request_ctrls.content
|
||||||
good_response = mock.MagicMock()
|
good_response = mock.MagicMock()
|
||||||
good_response.status_code = RUNTIME_VARS['status']
|
good_response.status_code = RUNTIME_VARS['status']
|
||||||
good_response.content = RUNTIME_VARS['login']
|
good_response.content = raw_controllers
|
||||||
|
|
||||||
def request_verify_cert(*args, **kwargs):
|
def request_verify_cert(*args, **kwargs):
|
||||||
self.assertEqual(kwargs['verify'], '/path/to/cert')
|
self.assertEqual(kwargs['verify'], '/path/to/cert')
|
||||||
@ -623,7 +633,30 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
request.side_effect = request_verify_cert
|
request.side_effect = request_verify_cert
|
||||||
self.driver.do_setup(None)
|
self.driver.do_setup(None)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request')
|
||||||
|
def test_wrong_access_key(self, request):
|
||||||
|
"""Wrong Access Key"""
|
||||||
|
fake_ak = 'FAKEACCESSKEY'
|
||||||
|
self.configuration.zadara_access_key = fake_ak
|
||||||
|
|
||||||
|
bad_response = mock.MagicMock()
|
||||||
|
bad_response.status_code = RUNTIME_VARS['status']
|
||||||
|
bad_response.content = RUNTIME_VARS['bad_login']
|
||||||
|
|
||||||
|
def request_verify_access_key(*args, **kwargs):
|
||||||
|
# Checks if the fake access_key was sent to driver
|
||||||
|
token = kwargs['headers']['X-Access-Key']
|
||||||
|
self.assertEqual(token, fake_ak, "access_key wasn't delivered")
|
||||||
|
return bad_response
|
||||||
|
|
||||||
|
request.side_effect = request_verify_access_key
|
||||||
|
# when access key is invalid, driver will raise
|
||||||
|
# ZadaraInvalidAccessKey exception
|
||||||
|
self.assertRaises(zadara.ZadaraInvalidAccessKey,
|
||||||
|
self.driver.do_setup,
|
||||||
|
None)
|
||||||
|
|
||||||
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_bad_http_response(self):
|
def test_bad_http_response(self):
|
||||||
"""Coverage test for non-good HTTP response."""
|
"""Coverage test for non-good HTTP response."""
|
||||||
RUNTIME_VARS['status'] = 400
|
RUNTIME_VARS['status'] = 400
|
||||||
@ -632,8 +665,8 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.assertRaises(exception.BadHTTPResponseStatus,
|
self.assertRaises(exception.BadHTTPResponseStatus,
|
||||||
self.driver.create_volume, volume)
|
self.driver.create_volume, volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_termiante_connection_force_detach(self):
|
def test_terminate_connection_force_detach(self):
|
||||||
"""Test terminate connection for os-force_detach """
|
"""Test terminate connection for os-force_detach """
|
||||||
volume = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
volume = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
||||||
connector = dict(initiator='test_iqn.1')
|
connector = dict(initiator='test_iqn.1')
|
||||||
@ -650,7 +683,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.driver.delete_volume(volume)
|
self.driver.delete_volume(volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_delete_without_detach(self):
|
def test_delete_without_detach(self):
|
||||||
"""Test volume deletion without detach."""
|
"""Test volume deletion without detach."""
|
||||||
|
|
||||||
@ -664,18 +697,19 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.initialize_connection(volume1, connector3)
|
self.driver.initialize_connection(volume1, connector3)
|
||||||
self.driver.delete_volume(volume1)
|
self.driver.delete_volume(volume1)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_no_active_ctrl(self):
|
def test_no_active_ctrl(self):
|
||||||
|
|
||||||
RUNTIME_VARS['controllers'] = []
|
|
||||||
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
||||||
connector = dict(initiator='test_iqn.1')
|
connector = dict(initiator='test_iqn.1')
|
||||||
self.driver.create_volume(volume)
|
self.driver.create_volume(volume)
|
||||||
|
|
||||||
|
RUNTIME_VARS['controllers'] = []
|
||||||
self.assertRaises(zadara.ZadaraVPSANoActiveController,
|
self.assertRaises(zadara.ZadaraVPSANoActiveController,
|
||||||
self.driver.initialize_connection,
|
self.driver.initialize_connection,
|
||||||
volume, connector)
|
volume, connector)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_create_destroy_snapshot(self):
|
def test_create_destroy_snapshot(self):
|
||||||
"""Create/Delete snapshot test."""
|
"""Create/Delete snapshot test."""
|
||||||
volume = {'name': 'test_volume_01', 'size': 1}
|
volume = {'name': 'test_volume_01', 'size': 1}
|
||||||
@ -700,7 +734,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.delete_snapshot(snapshot)
|
self.driver.delete_snapshot(snapshot)
|
||||||
self.driver.delete_volume(volume)
|
self.driver.delete_volume(volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_expand_volume(self):
|
def test_expand_volume(self):
|
||||||
"""Expand volume test."""
|
"""Expand volume test."""
|
||||||
volume = {'name': 'test_volume_01', 'size': 10}
|
volume = {'name': 'test_volume_01', 'size': 10}
|
||||||
@ -718,7 +752,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.extend_volume(volume, 15)
|
self.driver.extend_volume(volume, 15)
|
||||||
self.driver.delete_volume(volume)
|
self.driver.delete_volume(volume)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_create_destroy_clones(self):
|
def test_create_destroy_clones(self):
|
||||||
"""Create/Delete clones test."""
|
"""Create/Delete clones test."""
|
||||||
volume1 = {'name': 'test_volume_01', 'id': '01', 'size': 1}
|
volume1 = {'name': 'test_volume_01', 'id': '01', 'size': 1}
|
||||||
@ -757,7 +791,7 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
|||||||
self.driver.delete_snapshot(snapshot)
|
self.driver.delete_snapshot(snapshot)
|
||||||
self.driver.delete_volume(volume1)
|
self.driver.delete_volume(volume1)
|
||||||
|
|
||||||
@mock.patch.object(requests, 'request', FakeRequests)
|
@mock.patch.object(requests.Session, 'request', FakeRequests)
|
||||||
def test_get_volume_stats(self):
|
def test_get_volume_stats(self):
|
||||||
"""Get stats test."""
|
"""Get stats test."""
|
||||||
self.configuration.safe_get.return_value = 'ZadaraVPSAISCSIDriver'
|
self.configuration.safe_get.return_value = 'ZadaraVPSAISCSIDriver'
|
||||||
|
@ -12,8 +12,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""Volume driver for Zadara Virtual Private Storage Array (VPSA).
|
||||||
Volume driver for Zadara Virtual Private Storage Array (VPSA).
|
|
||||||
|
|
||||||
This driver requires VPSA with API version 15.07 or higher.
|
This driver requires VPSA with API version 15.07 or higher.
|
||||||
"""
|
"""
|
||||||
@ -52,10 +51,16 @@ zadara_opts = [
|
|||||||
'certificate of the VPSA endpoint.'),
|
'certificate of the VPSA endpoint.'),
|
||||||
cfg.StrOpt('zadara_user',
|
cfg.StrOpt('zadara_user',
|
||||||
default=None,
|
default=None,
|
||||||
|
deprecated_for_removal=True,
|
||||||
help='VPSA - Username'),
|
help='VPSA - Username'),
|
||||||
cfg.StrOpt('zadara_password',
|
cfg.StrOpt('zadara_password',
|
||||||
default=None,
|
default=None,
|
||||||
help='VPSA - Password',
|
help='VPSA - Password',
|
||||||
|
deprecated_for_removal=True,
|
||||||
|
secret=True),
|
||||||
|
cfg.StrOpt('zadara_access_key',
|
||||||
|
default=None,
|
||||||
|
help='VPSA access key',
|
||||||
secret=True),
|
secret=True),
|
||||||
cfg.StrOpt('zadara_vpsa_poolname',
|
cfg.StrOpt('zadara_vpsa_poolname',
|
||||||
default=None,
|
default=None,
|
||||||
@ -69,7 +74,6 @@ zadara_opts = [
|
|||||||
cfg.BoolOpt('zadara_default_snap_policy',
|
cfg.BoolOpt('zadara_default_snap_policy',
|
||||||
default=False,
|
default=False,
|
||||||
help="VPSA - Attach snapshot policy for volumes")]
|
help="VPSA - Attach snapshot policy for volumes")]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opts(zadara_opts, group=configuration.SHARED_CONF_GROUP)
|
CONF.register_opts(zadara_opts, group=configuration.SHARED_CONF_GROUP)
|
||||||
|
|
||||||
@ -98,24 +102,22 @@ class ZadaraVolumeNotFound(exception.VolumeDriverException):
|
|||||||
message = "%(reason)s"
|
message = "%(reason)s"
|
||||||
|
|
||||||
|
|
||||||
|
class ZadaraInvalidAccessKey(exception.VolumeDriverException):
|
||||||
|
message = "Invalid VPSA access key"
|
||||||
|
|
||||||
|
|
||||||
class ZadaraVPSAConnection(object):
|
class ZadaraVPSAConnection(object):
|
||||||
"""Executes volume driver commands on VPSA."""
|
"""Executes volume driver commands on VPSA."""
|
||||||
|
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.access_key = None
|
self.access_key = conf.zadara_access_key
|
||||||
|
|
||||||
self.ensure_connection()
|
self.ensure_connection()
|
||||||
|
|
||||||
def _generate_vpsa_cmd(self, cmd, **kwargs):
|
def _generate_vpsa_cmd(self, cmd, **kwargs):
|
||||||
"""Generate command to be sent to VPSA."""
|
"""Generate command to be sent to VPSA."""
|
||||||
|
|
||||||
def _joined_params(params):
|
|
||||||
param_str = []
|
|
||||||
for k, v in params.items():
|
|
||||||
param_str.append("%s=%s" % (k, v))
|
|
||||||
return '&'.join(param_str)
|
|
||||||
|
|
||||||
# Dictionary of applicable VPSA commands in the following format:
|
# Dictionary of applicable VPSA commands in the following format:
|
||||||
# 'command': (method, API_URL, {optional parameters})
|
# 'command': (method, API_URL, {optional parameters})
|
||||||
vpsa_commands = {
|
vpsa_commands = {
|
||||||
@ -123,7 +125,6 @@ class ZadaraVPSAConnection(object):
|
|||||||
'/api/users/login.xml',
|
'/api/users/login.xml',
|
||||||
{'user': self.conf.zadara_user,
|
{'user': self.conf.zadara_user,
|
||||||
'password': self.conf.zadara_password}),
|
'password': self.conf.zadara_password}),
|
||||||
|
|
||||||
# Volume operations
|
# Volume operations
|
||||||
'create_volume': ('POST',
|
'create_volume': ('POST',
|
||||||
'/api/volumes.xml',
|
'/api/volumes.xml',
|
||||||
@ -143,7 +144,6 @@ class ZadaraVPSAConnection(object):
|
|||||||
'/api/volumes/%s/expand.xml'
|
'/api/volumes/%s/expand.xml'
|
||||||
% kwargs.get('vpsa_vol'),
|
% kwargs.get('vpsa_vol'),
|
||||||
{'capacity': kwargs.get('size')}),
|
{'capacity': kwargs.get('size')}),
|
||||||
|
|
||||||
# Snapshot operations
|
# Snapshot operations
|
||||||
# Snapshot request is triggered for a single volume though the
|
# Snapshot request is triggered for a single volume though the
|
||||||
# API call implies that snapshot is triggered for CG (legacy API).
|
# API call implies that snapshot is triggered for CG (legacy API).
|
||||||
@ -155,24 +155,20 @@ class ZadaraVPSAConnection(object):
|
|||||||
'/api/snapshots/%s.xml'
|
'/api/snapshots/%s.xml'
|
||||||
% kwargs.get('snap_id'),
|
% kwargs.get('snap_id'),
|
||||||
{}),
|
{}),
|
||||||
|
|
||||||
'create_clone_from_snap': ('POST',
|
'create_clone_from_snap': ('POST',
|
||||||
'/api/consistency_groups/%s/clone.xml'
|
'/api/consistency_groups/%s/clone.xml'
|
||||||
% kwargs.get('cg_name'),
|
% kwargs.get('cg_name'),
|
||||||
{'name': kwargs.get('name'),
|
{'name': kwargs.get('name'),
|
||||||
'snapshot': kwargs.get('snap_id')}),
|
'snapshot': kwargs.get('snap_id')}),
|
||||||
|
|
||||||
'create_clone': ('POST',
|
'create_clone': ('POST',
|
||||||
'/api/consistency_groups/%s/clone.xml'
|
'/api/consistency_groups/%s/clone.xml'
|
||||||
% kwargs.get('cg_name'),
|
% kwargs.get('cg_name'),
|
||||||
{'name': kwargs.get('name')}),
|
{'name': kwargs.get('name')}),
|
||||||
|
|
||||||
# Server operations
|
# Server operations
|
||||||
'create_server': ('POST',
|
'create_server': ('POST',
|
||||||
'/api/servers.xml',
|
'/api/servers.xml',
|
||||||
{'display_name': kwargs.get('initiator'),
|
{'display_name': kwargs.get('initiator'),
|
||||||
'iqn': kwargs.get('initiator')}),
|
'iqn': kwargs.get('initiator')}),
|
||||||
|
|
||||||
# Attach/Detach operations
|
# Attach/Detach operations
|
||||||
'attach_volume': ('POST',
|
'attach_volume': ('POST',
|
||||||
'/api/servers/%s/volumes.xml'
|
'/api/servers/%s/volumes.xml'
|
||||||
@ -184,7 +180,6 @@ class ZadaraVPSAConnection(object):
|
|||||||
% kwargs.get('vpsa_vol'),
|
% kwargs.get('vpsa_vol'),
|
||||||
{'server_name[]': kwargs.get('vpsa_srv'),
|
{'server_name[]': kwargs.get('vpsa_srv'),
|
||||||
'force': 'YES'}),
|
'force': 'YES'}),
|
||||||
|
|
||||||
# Get operations
|
# Get operations
|
||||||
'list_volumes': ('GET',
|
'list_volumes': ('GET',
|
||||||
'/api/volumes.xml',
|
'/api/volumes.xml',
|
||||||
@ -207,28 +202,18 @@ class ZadaraVPSAConnection(object):
|
|||||||
% kwargs.get('cg_name'),
|
% kwargs.get('cg_name'),
|
||||||
{})}
|
{})}
|
||||||
|
|
||||||
if cmd not in vpsa_commands:
|
try:
|
||||||
|
method, url, params = vpsa_commands[cmd]
|
||||||
|
except KeyError:
|
||||||
raise exception.UnknownCmd(cmd=cmd)
|
raise exception.UnknownCmd(cmd=cmd)
|
||||||
else:
|
|
||||||
(method, url, params) = vpsa_commands[cmd]
|
|
||||||
|
|
||||||
if method == 'GET':
|
if method == 'GET':
|
||||||
# For GET commands add parameters to the URL
|
params = dict(page=1, start=0, limit=0)
|
||||||
params.update(dict(access_key=self.access_key,
|
body = None
|
||||||
page=1, start=0, limit=0))
|
|
||||||
url += '?' + _joined_params(params)
|
|
||||||
body = ''
|
|
||||||
|
|
||||||
elif method == 'DELETE':
|
elif method in ['DELETE', 'POST']:
|
||||||
# For DELETE commands add parameters to the URL
|
body = params
|
||||||
params.update(dict(access_key=self.access_key))
|
params = None
|
||||||
url += '?' + _joined_params(params)
|
|
||||||
body = ''
|
|
||||||
|
|
||||||
elif method == 'POST':
|
|
||||||
if self.access_key:
|
|
||||||
params.update(dict(access_key=self.access_key))
|
|
||||||
body = _joined_params(params)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = (_('Method %(method)s is not defined') %
|
msg = (_('Method %(method)s is not defined') %
|
||||||
@ -236,7 +221,11 @@ class ZadaraVPSAConnection(object):
|
|||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise AssertionError(msg)
|
raise AssertionError(msg)
|
||||||
|
|
||||||
return (method, url, body)
|
# 'access_key' was generated using username and password
|
||||||
|
# or it was taken from the input file
|
||||||
|
headers = {'X-Access-Key': self.access_key}
|
||||||
|
|
||||||
|
return method, url, params, body, headers
|
||||||
|
|
||||||
def ensure_connection(self, cmd=None):
|
def ensure_connection(self, cmd=None):
|
||||||
"""Retrieve access key for VPSA connection."""
|
"""Retrieve access key for VPSA connection."""
|
||||||
@ -248,12 +237,12 @@ class ZadaraVPSAConnection(object):
|
|||||||
xml_tree = self.send_cmd(cmd)
|
xml_tree = self.send_cmd(cmd)
|
||||||
user = xml_tree.find('user')
|
user = xml_tree.find('user')
|
||||||
if user is None:
|
if user is None:
|
||||||
raise (exception.MalformedResponse(cmd=cmd,
|
raise (exception.MalformedResponse(
|
||||||
reason=_('no "user" field')))
|
cmd=cmd, reason=_('no "user" field')))
|
||||||
access_key = user.findtext('access-key')
|
access_key = user.findtext('access-key')
|
||||||
if access_key is None:
|
if access_key is None:
|
||||||
raise (exception.MalformedResponse(cmd=cmd,
|
raise (exception.MalformedResponse(
|
||||||
reason=_('no "access-key" field')))
|
cmd=cmd, reason=_('no "access-key" field')))
|
||||||
self.access_key = access_key
|
self.access_key = access_key
|
||||||
|
|
||||||
def send_cmd(self, cmd, **kwargs):
|
def send_cmd(self, cmd, **kwargs):
|
||||||
@ -261,7 +250,8 @@ class ZadaraVPSAConnection(object):
|
|||||||
|
|
||||||
self.ensure_connection(cmd)
|
self.ensure_connection(cmd)
|
||||||
|
|
||||||
(method, url, body) = self._generate_vpsa_cmd(cmd, **kwargs)
|
method, url, params, body, headers = self._generate_vpsa_cmd(cmd,
|
||||||
|
**kwargs)
|
||||||
LOG.debug('Invoking %(cmd)s using %(method)s request.',
|
LOG.debug('Invoking %(cmd)s using %(method)s request.',
|
||||||
{'cmd': cmd, 'method': method})
|
{'cmd': cmd, 'method': method})
|
||||||
|
|
||||||
@ -284,7 +274,10 @@ class ZadaraVPSAConnection(object):
|
|||||||
api_url = "%s://%s%s" % (protocol, host, url)
|
api_url = "%s://%s%s" % (protocol, host, url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.request(method, api_url, data=body,
|
with requests.Session() as session:
|
||||||
|
session.headers.update(headers)
|
||||||
|
response = session.request(method, api_url, params=params,
|
||||||
|
data=body, headers=headers,
|
||||||
verify=verify)
|
verify=verify)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
message = (_('Exception: %s') % six.text_type(e))
|
message = (_('Exception: %s') % six.text_type(e))
|
||||||
@ -296,6 +289,10 @@ class ZadaraVPSAConnection(object):
|
|||||||
data = response.content
|
data = response.content
|
||||||
xml_tree = lxml.fromstring(data)
|
xml_tree = lxml.fromstring(data)
|
||||||
status = xml_tree.findtext('status')
|
status = xml_tree.findtext('status')
|
||||||
|
if status == '5':
|
||||||
|
# Invalid Credentials
|
||||||
|
raise ZadaraInvalidAccessKey()
|
||||||
|
|
||||||
if status != '0':
|
if status != '0':
|
||||||
raise exception.FailedCmdWithDump(status=status, data=data)
|
raise exception.FailedCmdWithDump(status=status, data=data)
|
||||||
|
|
||||||
@ -314,15 +311,17 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
Version history:
|
Version history:
|
||||||
15.07 - Initial driver
|
15.07 - Initial driver
|
||||||
16.05 - Move from httplib to requests
|
16.05 - Move from httplib to requests
|
||||||
|
19.08 - Add API access key authentication option
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = '16.05'
|
VERSION = '19.08'
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
# ThirdPartySystems wiki page
|
||||||
CI_WIKI_NAME = "ZadaraStorage_VPSA_CI"
|
CI_WIKI_NAME = "ZadaraStorage_VPSA_CI"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
|
super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
|
||||||
|
self.vpsa = None
|
||||||
self.configuration.append_config_values(zadara_opts)
|
self.configuration.append_config_values(zadara_opts)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -335,10 +334,20 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
Establishes initial connection with VPSA and retrieves access_key.
|
Establishes initial connection with VPSA and retrieves access_key.
|
||||||
"""
|
"""
|
||||||
self.vpsa = ZadaraVPSAConnection(self.configuration)
|
self.vpsa = ZadaraVPSAConnection(self.configuration)
|
||||||
|
self._check_access_key_validity()
|
||||||
|
|
||||||
|
def _check_access_key_validity(self):
|
||||||
|
"""Check VPSA access key"""
|
||||||
|
self.vpsa.ensure_connection()
|
||||||
|
if not self.vpsa.access_key:
|
||||||
|
raise ZadaraInvalidAccessKey()
|
||||||
|
active_ctrl = self._get_active_controller_details()
|
||||||
|
if active_ctrl is None:
|
||||||
|
raise ZadaraInvalidAccessKey()
|
||||||
|
|
||||||
def check_for_setup_error(self):
|
def check_for_setup_error(self):
|
||||||
"""Returns an error (exception) if prerequisites aren't met."""
|
"""Returns an error (exception) if prerequisites aren't met."""
|
||||||
self.vpsa.ensure_connection()
|
self._check_access_key_validity()
|
||||||
|
|
||||||
def local_path(self, volume):
|
def local_path(self, volume):
|
||||||
"""Return local path to existing local volume."""
|
"""Return local path to existing local volume."""
|
||||||
@ -358,14 +367,14 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
result_list = []
|
result_list = []
|
||||||
(key, value) = search_tuple
|
key, value = search_tuple
|
||||||
for object in objects.getchildren():
|
for child_object in objects.getchildren():
|
||||||
found_value = object.findtext(key)
|
found_value = child_object.findtext(key)
|
||||||
if found_value and (found_value == value or value is None):
|
if found_value and (found_value == value or value is None):
|
||||||
if first:
|
if first:
|
||||||
return object
|
return child_object
|
||||||
else:
|
else:
|
||||||
result_list.append(object)
|
result_list.append(child_object)
|
||||||
return result_list if result_list else None
|
return result_list if result_list else None
|
||||||
|
|
||||||
def _get_vpsa_volume_name_and_size(self, name):
|
def _get_vpsa_volume_name_and_size(self, name):
|
||||||
@ -377,7 +386,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
return (volume.findtext('name'),
|
return (volume.findtext('name'),
|
||||||
int(volume.findtext('virtual-capacity')))
|
int(volume.findtext('virtual-capacity')))
|
||||||
|
|
||||||
return (None, None)
|
return None, None
|
||||||
|
|
||||||
def _get_vpsa_volume_name(self, name):
|
def _get_vpsa_volume_name(self, name):
|
||||||
"""Return VPSA's name for the volume."""
|
"""Return VPSA's name for the volume."""
|
||||||
@ -419,9 +428,9 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
free = int(float(pool.findtext('available-capacity')))
|
free = int(float(pool.findtext('available-capacity')))
|
||||||
LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
|
LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
|
||||||
{'name': pool_name, 'total': total, 'free': free})
|
{'name': pool_name, 'total': total, 'free': free})
|
||||||
return (total, free)
|
return total, free
|
||||||
|
|
||||||
return ('unknown', 'unknown')
|
return 'unknown', 'unknown'
|
||||||
|
|
||||||
def _get_active_controller_details(self):
|
def _get_active_controller_details(self):
|
||||||
"""Return details of VPSA's active controller."""
|
"""Return details of VPSA's active controller."""
|
||||||
@ -435,14 +444,18 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
chap_passwd=ctrl.findtext('vpsa-chap-secret'))
|
chap_passwd=ctrl.findtext('vpsa-chap-secret'))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _detach_vpsa_volume(self, vpsa_vol):
|
def _detach_vpsa_volume(self, vpsa_vol, vpsa_srv=None):
|
||||||
"""Detach volume from all attached servers."""
|
"""Detach volume from all attached servers."""
|
||||||
|
if vpsa_srv:
|
||||||
|
list_servers_ids = [vpsa_srv]
|
||||||
|
else:
|
||||||
list_servers = self._get_servers_attached_to_volume(vpsa_vol)
|
list_servers = self._get_servers_attached_to_volume(vpsa_vol)
|
||||||
for server in list_servers:
|
list_servers_ids = [s.findtext('name') for s in list_servers]
|
||||||
|
|
||||||
|
for server_id in list_servers_ids:
|
||||||
# Detach volume from server
|
# Detach volume from server
|
||||||
vpsa_srv = server.findtext('name')
|
|
||||||
self.vpsa.send_cmd('detach_volume',
|
self.vpsa.send_cmd('detach_volume',
|
||||||
vpsa_srv=vpsa_srv,
|
vpsa_srv=server_id,
|
||||||
vpsa_vol=vpsa_vol)
|
vpsa_vol=vpsa_vol)
|
||||||
|
|
||||||
def _get_server_name(self, initiator):
|
def _get_server_name(self, initiator):
|
||||||
@ -482,7 +495,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
'It might be already deleted', name)
|
'It might be already deleted', name)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._detach_vpsa_volume(vpsa_vol)
|
self._detach_vpsa_volume(vpsa_vol=vpsa_vol)
|
||||||
|
|
||||||
# Delete volume
|
# Delete volume
|
||||||
self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
|
self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
|
||||||
@ -556,7 +569,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
% volume['name'],
|
% volume['name'],
|
||||||
snap_id=snap_id)
|
snap_id=snap_id)
|
||||||
|
|
||||||
if (volume['size'] > snapshot['volume_size']):
|
if volume['size'] > snapshot['volume_size']:
|
||||||
self.extend_volume(volume, volume['size'])
|
self.extend_volume(volume, volume['size'])
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
@ -577,7 +590,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
name=self.configuration.zadara_vol_name_template
|
name=self.configuration.zadara_vol_name_template
|
||||||
% volume['name'])
|
% volume['name'])
|
||||||
|
|
||||||
if (volume['size'] > src_vref['size']):
|
if volume['size'] > src_vref['size']:
|
||||||
self.extend_volume(volume, volume['size'])
|
self.extend_volume(volume, volume['size'])
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
def extend_volume(self, volume, new_size):
|
||||||
@ -618,10 +631,13 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
|
|
||||||
During this call VPSA exposes volume to particular Initiator. It also
|
During this call VPSA exposes volume to particular Initiator. It also
|
||||||
creates a 'server' entity for Initiator (if it was not created before)
|
creates a 'server' entity for Initiator (if it was not created before)
|
||||||
|
|
||||||
All necessary connection information is returned, including auth data.
|
All necessary connection information is returned, including auth data.
|
||||||
Connection data (target, LUN) is not stored in the DB.
|
Connection data (target, LUN) is not stored in the DB.
|
||||||
"""
|
"""
|
||||||
|
# First: Check Active controller: if not valid, raise exception
|
||||||
|
ctrl = self._get_active_controller_details()
|
||||||
|
if not ctrl:
|
||||||
|
raise ZadaraVPSANoActiveController()
|
||||||
|
|
||||||
# Get/Create server name for IQN
|
# Get/Create server name for IQN
|
||||||
initiator_name = connector['initiator']
|
initiator_name = connector['initiator']
|
||||||
@ -635,11 +651,6 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
if not vpsa_vol:
|
if not vpsa_vol:
|
||||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||||
|
|
||||||
# Get Active controller details
|
|
||||||
ctrl = self._get_active_controller_details()
|
|
||||||
if not ctrl:
|
|
||||||
raise ZadaraVPSANoActiveController()
|
|
||||||
|
|
||||||
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
||||||
vpsa_vol=vpsa_vol)
|
vpsa_vol=vpsa_vol)
|
||||||
attach = self._xml_parse_helper(xml_tree, 'servers',
|
attach = self._xml_parse_helper(xml_tree, 'servers',
|
||||||
@ -649,30 +660,30 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
self.vpsa.send_cmd('attach_volume',
|
self.vpsa.send_cmd('attach_volume',
|
||||||
vpsa_srv=vpsa_srv,
|
vpsa_srv=vpsa_srv,
|
||||||
vpsa_vol=vpsa_vol)
|
vpsa_vol=vpsa_vol)
|
||||||
# Get connection info
|
|
||||||
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
||||||
vpsa_vol=vpsa_vol)
|
vpsa_vol=vpsa_vol)
|
||||||
server = self._xml_parse_helper(xml_tree, 'servers',
|
server = self._xml_parse_helper(xml_tree, 'servers',
|
||||||
('iqn', initiator_name))
|
('iqn', initiator_name))
|
||||||
if server is None:
|
if server is None:
|
||||||
raise ZadaraAttachmentsNotFound(name=name)
|
raise ZadaraAttachmentsNotFound(name=name)
|
||||||
|
|
||||||
target = server.findtext('target')
|
target = server.findtext('target')
|
||||||
lun = int(server.findtext('lun'))
|
lun = int(server.findtext('lun'))
|
||||||
if target is None or lun is None:
|
if None in [target, lun]:
|
||||||
raise ZadaraInvalidAttachmentInfo(
|
raise ZadaraInvalidAttachmentInfo(
|
||||||
name=name,
|
name=name,
|
||||||
reason=_('target=%(target)s, lun=%(lun)s') %
|
reason=_('target=%(target)s, lun=%(lun)s') %
|
||||||
{'target': target, 'lun': lun})
|
{'target': target, 'lun': lun})
|
||||||
|
|
||||||
properties = {}
|
properties = {'target_discovered': False,
|
||||||
properties['target_discovered'] = False
|
'target_portal': '%s:%s' % (ctrl['ip'], '3260'),
|
||||||
properties['target_portal'] = '%s:%s' % (ctrl['ip'], '3260')
|
'target_iqn': target,
|
||||||
properties['target_iqn'] = target
|
'target_lun': lun,
|
||||||
properties['target_lun'] = lun
|
'volume_id': volume['id'],
|
||||||
properties['volume_id'] = volume['id']
|
'auth_method': 'CHAP',
|
||||||
properties['auth_method'] = 'CHAP'
|
'auth_username': ctrl['chap_user'],
|
||||||
properties['auth_username'] = ctrl['chap_user']
|
'auth_password': ctrl['chap_passwd']}
|
||||||
properties['auth_password'] = ctrl['chap_passwd']
|
|
||||||
|
|
||||||
LOG.debug('Attach properties: %(properties)s',
|
LOG.debug('Attach properties: %(properties)s',
|
||||||
{'properties': strutils.mask_password(properties)})
|
{'properties': strutils.mask_password(properties)})
|
||||||
@ -682,14 +693,19 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
|
|
||||||
def terminate_connection(self, volume, connector, **kwargs):
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
"""Detach volume from the initiator."""
|
"""Detach volume from the initiator."""
|
||||||
|
|
||||||
# Get server name for IQN
|
# Get server name for IQN
|
||||||
if connector is None:
|
if connector is None:
|
||||||
# Detach volume from all servers
|
# Detach volume from all servers
|
||||||
# Get volume name
|
# Get volume name
|
||||||
name = self.configuration.zadara_vol_name_template % volume['name']
|
name = self.configuration.zadara_vol_name_template % volume['name']
|
||||||
vpsa_vol = self._get_vpsa_volume_name(name)
|
vpsa_vol = self._get_vpsa_volume_name(name)
|
||||||
self._detach_vpsa_volume(vpsa_vol)
|
if vpsa_vol:
|
||||||
|
self._detach_vpsa_volume(vpsa_vol=vpsa_vol)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
LOG.warning('Volume %s could not be found', name)
|
||||||
|
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||||
|
|
||||||
initiator_name = connector['initiator']
|
initiator_name = connector['initiator']
|
||||||
|
|
||||||
@ -704,9 +720,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
|||||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||||
|
|
||||||
# Detach volume from server
|
# Detach volume from server
|
||||||
self.vpsa.send_cmd('detach_volume',
|
self._detach_vpsa_volume(vpsa_vol=vpsa_vol, vpsa_srv=vpsa_srv)
|
||||||
vpsa_srv=vpsa_srv,
|
|
||||||
vpsa_vol=vpsa_vol)
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
def get_volume_stats(self, refresh=False):
|
||||||
"""Get volume stats.
|
"""Get volume stats.
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Zadara VPSA Driver: Added new driver authentication method to use VPSA
|
||||||
|
API access key, and deprecate exisiting authentication method that used
|
||||||
|
username and password combination. The deprecated config inputs will be
|
||||||
|
removed in the next official release after Train.
|
||||||
|
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
Add a new config option 'zadara_access_key': Zadara VPSA access key.
|
Loading…
Reference in New Issue
Block a user