Merge "Add backend driver for Zadara Storage VPSA"
This commit is contained in:
commit
401a8a4a04
@ -1043,6 +1043,35 @@ class ViolinBackendErrNotFound(CinderException):
|
||||
message = _("Backend reports: item not found")
|
||||
|
||||
|
||||
class BadHTTPResponseStatus(VolumeDriverException):
|
||||
message = _("Bad HTTP response status %(status)s")
|
||||
|
||||
|
||||
# ZADARA STORAGE VPSA driver exception
|
||||
class ZadaraServerCreateFailure(VolumeDriverException):
|
||||
message = _("Unable to create server object for initiator %(name)s")
|
||||
|
||||
|
||||
class ZadaraServerNotFound(NotFound):
|
||||
message = _("Unable to find server object for initiator %(name)s")
|
||||
|
||||
|
||||
class ZadaraVPSANoActiveController(VolumeDriverException):
|
||||
message = _("Unable to find any active VPSA controller")
|
||||
|
||||
|
||||
class ZadaraAttachmentsNotFound(NotFound):
|
||||
message = _("Failed to retrieve attachments for volume %(name)s")
|
||||
|
||||
|
||||
class ZadaraInvalidAttachmentInfo(Invalid):
|
||||
message = _("Invalid attachment info for volume %(name)s: %(reason)s")
|
||||
|
||||
|
||||
class ZadaraVolumeNotFound(VolumeDriverException):
|
||||
message = _("%(reason)s")
|
||||
|
||||
|
||||
# ZFSSA NFS driver exception.
|
||||
class WebDAVClientError(CinderException):
|
||||
message = _("The WebDAV request failed. Reason: %(msg)s, "
|
||||
|
@ -160,6 +160,7 @@ from cinder.volume.drivers import vzstorage as cinder_volume_drivers_vzstorage
|
||||
from cinder.volume.drivers.windows import windows as \
|
||||
cinder_volume_drivers_windows_windows
|
||||
from cinder.volume.drivers import xio as cinder_volume_drivers_xio
|
||||
from cinder.volume.drivers import zadara as cinder_volume_drivers_zadara
|
||||
from cinder.volume.drivers.zfssa import zfssaiscsi as \
|
||||
cinder_volume_drivers_zfssa_zfssaiscsi
|
||||
from cinder.volume.drivers.zfssa import zfssanfs as \
|
||||
@ -337,6 +338,7 @@ def list_opts():
|
||||
cinder_volume_drivers_ibm_xivds8k.xiv_ds8k_opts,
|
||||
cinder_volume_drivers_hpe_hpe3parcommon.hpe3par_opts,
|
||||
cinder_volume_drivers_datera.d_opts,
|
||||
cinder_volume_drivers_zadara.zadara_opts,
|
||||
cinder_volume_drivers_blockdevice.volume_opts,
|
||||
cinder_volume_drivers_quobyte.volume_opts,
|
||||
cinder_volume_drivers_vzstorage.vzstorage_opts,
|
||||
|
779
cinder/tests/unit/test_zadara.py
Normal file
779
cinder/tests/unit/test_zadara.py
Normal file
@ -0,0 +1,779 @@
|
||||
# Copyright (c) 2016 Zadara Storage, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
Tests for Zadara VPSA volume driver
|
||||
"""
|
||||
import copy
|
||||
|
||||
import mock
|
||||
from six.moves import http_client
|
||||
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers import zadara
|
||||
|
||||
DEFAULT_RUNTIME_VARS = {
|
||||
'status': 200,
|
||||
'user': 'test',
|
||||
'password': 'test_password',
|
||||
'access_key': '0123456789ABCDEF',
|
||||
'volumes': [],
|
||||
'servers': [],
|
||||
'controllers': [('active_ctrl', {'display-name': 'test_ctrl'})],
|
||||
'counter': 1000,
|
||||
|
||||
'login': """
|
||||
<hash>
|
||||
<user>
|
||||
<updated-at type="datetime">2012-04-30...</updated-at>
|
||||
<access-key>%s</access-key>
|
||||
<id type="integer">1</id>
|
||||
<created-at type="datetime">2012-02-21...</created-at>
|
||||
<email>jsmith@example.com</email>
|
||||
<username>jsmith</username>
|
||||
</user>
|
||||
<status type="integer">0</status>
|
||||
</hash>""",
|
||||
|
||||
'good': """
|
||||
<hash>
|
||||
<status type="integer">0</status>
|
||||
</hash>""",
|
||||
|
||||
'bad_login': """
|
||||
<hash>
|
||||
<status type="integer">5</status>
|
||||
<status-msg>Some message...</status-msg>
|
||||
</hash>""",
|
||||
|
||||
'bad_volume': """
|
||||
<hash>
|
||||
<status type="integer">10081</status>
|
||||
<status-msg>Virtual volume xxx not found</status-msg>
|
||||
</hash>""",
|
||||
|
||||
'bad_server': """
|
||||
<hash>
|
||||
<status type="integer">10086</status>
|
||||
<status-msg>Server xxx not found</status-msg>
|
||||
</hash>""",
|
||||
|
||||
'server_created': """
|
||||
<create-server-response>
|
||||
<server-name>%s</server-name>
|
||||
<status type='integer'>0</status>
|
||||
</create-server-response>""",
|
||||
}
|
||||
|
||||
RUNTIME_VARS = None
|
||||
|
||||
|
||||
class FakeRequest(object):
|
||||
def __init__(self, method, url, body):
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.body = body
|
||||
self.status = RUNTIME_VARS['status']
|
||||
|
||||
def read(self):
|
||||
ops = {'POST': [('/api/users/login.xml', self._login),
|
||||
('/api/volumes.xml', self._create_volume),
|
||||
('/api/servers.xml', self._create_server),
|
||||
('/api/servers/*/volumes.xml', self._attach),
|
||||
('/api/volumes/*/detach.xml', self._detach),
|
||||
('/api/volumes/*/expand.xml', self._expand),
|
||||
('/api/consistency_groups/*/snapshots.xml',
|
||||
self._create_snapshot),
|
||||
('/api/consistency_groups/*/clone.xml',
|
||||
self._create_clone)],
|
||||
'DELETE': [('/api/volumes/*', self._delete),
|
||||
('/api/snapshots/*', self._delete_snapshot)],
|
||||
'GET': [('/api/volumes.xml', self._list_volumes),
|
||||
('/api/pools.xml', self._list_pools),
|
||||
('/api/vcontrollers.xml', self._list_controllers),
|
||||
('/api/servers.xml', self._list_servers),
|
||||
('/api/consistency_groups/*/snapshots.xml',
|
||||
self._list_vol_snapshots),
|
||||
('/api/volumes/*/servers.xml',
|
||||
self._list_vol_attachments)]
|
||||
}
|
||||
|
||||
ops_list = ops[self.method]
|
||||
modified_url = self.url.split('?')[0]
|
||||
for (templ_url, func) in ops_list:
|
||||
if self._compare_url(modified_url, templ_url):
|
||||
result = func()
|
||||
return result
|
||||
|
||||
def _compare_url(self, url, template_url):
|
||||
items = url.split('/')
|
||||
titems = template_url.split('/')
|
||||
for (i, titem) in enumerate(titems):
|
||||
if titem != '*' and titem != items[i]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_parameters(self, data):
|
||||
items = data.split('&')
|
||||
params = {}
|
||||
for item in items:
|
||||
if item:
|
||||
(k, v) = item.split('=')
|
||||
params[k] = v
|
||||
return params
|
||||
|
||||
def _get_counter(self):
|
||||
cnt = RUNTIME_VARS['counter']
|
||||
RUNTIME_VARS['counter'] += 1
|
||||
return cnt
|
||||
|
||||
def _login(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if (params['user'] == RUNTIME_VARS['user'] and
|
||||
params['password'] == RUNTIME_VARS['password']):
|
||||
return RUNTIME_VARS['login'] % RUNTIME_VARS['access_key']
|
||||
else:
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
def _incorrect_access_key(self, params):
|
||||
return (params['access_key'] != RUNTIME_VARS['access_key'])
|
||||
|
||||
def _create_volume(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if self._incorrect_access_key(params):
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
params['display-name'] = params['name']
|
||||
params['cg-name'] = params['name']
|
||||
params['snapshots'] = []
|
||||
params['attachments'] = []
|
||||
vpsa_vol = 'volume-%07d' % self._get_counter()
|
||||
RUNTIME_VARS['volumes'].append((vpsa_vol, params))
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
def _create_server(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if self._incorrect_access_key(params):
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
params['display-name'] = params['display_name']
|
||||
vpsa_srv = 'srv-%07d' % self._get_counter()
|
||||
RUNTIME_VARS['servers'].append((vpsa_srv, params))
|
||||
return RUNTIME_VARS['server_created'] % vpsa_srv
|
||||
|
||||
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]
|
||||
vol = params['volume_name[]']
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if vol_name == vol:
|
||||
attachments = params['attachments']
|
||||
if srv in attachments:
|
||||
# already attached - ok
|
||||
return RUNTIME_VARS['good']
|
||||
else:
|
||||
attachments.append(srv)
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _detach(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if self._incorrect_access_key(params):
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
vol = self.url.split('/')[3]
|
||||
srv = params['server_name[]']
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if vol_name == vol:
|
||||
attachments = params['attachments']
|
||||
if srv not in attachments:
|
||||
return RUNTIME_VARS['bad_server']
|
||||
else:
|
||||
attachments.remove(srv)
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _expand(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if self._incorrect_access_key(params):
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
vol = self.url.split('/')[3]
|
||||
capacity = params['capacity']
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if vol_name == vol:
|
||||
params['capacity'] = capacity
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _create_snapshot(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if self._incorrect_access_key(params):
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
cg_name = self.url.split('/')[3]
|
||||
snap_name = params['display_name']
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if params['cg-name'] == cg_name:
|
||||
snapshots = params['snapshots']
|
||||
if snap_name in snapshots:
|
||||
# already attached
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
else:
|
||||
snapshots.append(snap_name)
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _delete_snapshot(self):
|
||||
snap = self.url.split('/')[3].split('.')[0]
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if snap in params['snapshots']:
|
||||
params['snapshots'].remove(snap)
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _create_clone(self):
|
||||
params = self._get_parameters(self.body)
|
||||
if self._incorrect_access_key(params):
|
||||
return RUNTIME_VARS['bad_login']
|
||||
|
||||
params['display-name'] = params['name']
|
||||
params['cg-name'] = params['name']
|
||||
params['capacity'] = 1
|
||||
params['snapshots'] = []
|
||||
params['attachments'] = []
|
||||
vpsa_vol = 'volume-%07d' % self._get_counter()
|
||||
RUNTIME_VARS['volumes'].append((vpsa_vol, params))
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
def _delete(self):
|
||||
vol = self.url.split('/')[3].split('.')[0]
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if vol_name == vol:
|
||||
if params['attachments']:
|
||||
# there are attachments - should be volume busy error
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
else:
|
||||
RUNTIME_VARS['volumes'].remove((vol_name, params))
|
||||
return RUNTIME_VARS['good']
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _generate_list_resp(self, header, footer, body, lst, vol):
|
||||
resp = header
|
||||
for (obj, params) in lst:
|
||||
if vol:
|
||||
resp += body % (obj,
|
||||
params['display-name'],
|
||||
params['cg-name'],
|
||||
params['capacity'])
|
||||
else:
|
||||
resp += body % (obj, params['display-name'])
|
||||
resp += footer
|
||||
return resp
|
||||
|
||||
def _list_volumes(self):
|
||||
header = """<show-volumes-response>
|
||||
<status type='integer'>0</status>
|
||||
<volumes type='array'>"""
|
||||
footer = "</volumes></show-volumes-response>"
|
||||
body = """<volume>
|
||||
<name>%s</name>
|
||||
<display-name>%s</display-name>
|
||||
<cg-name>%s</cg-name>
|
||||
<status>Available</status>
|
||||
<virtual-capacity type='integer'>%s</virtual-capacity>
|
||||
<allocated-capacity type='integer'>1</allocated-capacity>
|
||||
<raid-group-name>r5</raid-group-name>
|
||||
<cache>write-through</cache>
|
||||
<created-at type='datetime'>2012-01-28...</created-at>
|
||||
<modified-at type='datetime'>2012-01-28...</modified-at>
|
||||
</volume>"""
|
||||
return self._generate_list_resp(header,
|
||||
footer,
|
||||
body,
|
||||
RUNTIME_VARS['volumes'],
|
||||
True)
|
||||
|
||||
def _list_controllers(self):
|
||||
header = """<show-vcontrollers-response>
|
||||
<status type='integer'>0</status>
|
||||
<vcontrollers type='array'>"""
|
||||
footer = "</vcontrollers></show-vcontrollers-response>"
|
||||
body = """<vcontroller>
|
||||
<name>%s</name>
|
||||
<display-name>%s</display-name>
|
||||
<state>active</state>
|
||||
<target>iqn.2011-04.com.zadarastorage:vsa-xxx:1</target>
|
||||
<iscsi-ip>1.1.1.1</iscsi-ip>
|
||||
<mgmt-ip>1.1.1.1</mgmt-ip>
|
||||
<software-ver>0.0.09-05.1--77.7</software-ver>
|
||||
<heartbeat1>ok</heartbeat1>
|
||||
<heartbeat2>ok</heartbeat2>
|
||||
<vpsa-chap-user>test_chap_user</vpsa-chap-user>
|
||||
<vpsa-chap-secret>test_chap_secret</vpsa-chap-secret>
|
||||
</vcontroller>"""
|
||||
return self._generate_list_resp(header,
|
||||
footer,
|
||||
body,
|
||||
RUNTIME_VARS['controllers'],
|
||||
False)
|
||||
|
||||
def _list_pools(self):
|
||||
header = """<show-pools-response>
|
||||
<status type="integer">0</status>
|
||||
<pools type="array">
|
||||
"""
|
||||
footer = "</pools></show-pools-response>"
|
||||
return header + footer
|
||||
|
||||
def _list_servers(self):
|
||||
header = """<show-servers-response>
|
||||
<status type='integer'>0</status>
|
||||
<servers type='array'>"""
|
||||
footer = "</servers></show-servers-response>"
|
||||
body = """<server>
|
||||
<name>%s</name>
|
||||
<display-name>%s</display-name>
|
||||
<iqn>%s</iqn>
|
||||
<status>Active</status>
|
||||
<created-at type='datetime'>2012-01-28...</created-at>
|
||||
<modified-at type='datetime'>2012-01-28...</modified-at>
|
||||
</server>"""
|
||||
|
||||
resp = header
|
||||
for (obj, params) in RUNTIME_VARS['servers']:
|
||||
resp += body % (obj, params['display-name'], params['iqn'])
|
||||
resp += footer
|
||||
return resp
|
||||
|
||||
def _get_server_obj(self, name):
|
||||
for (srv_name, params) in RUNTIME_VARS['servers']:
|
||||
if srv_name == name:
|
||||
return params
|
||||
|
||||
def _list_vol_attachments(self):
|
||||
vol = self.url.split('/')[3]
|
||||
|
||||
header = """<show-servers-response>
|
||||
<status type="integer">0</status>
|
||||
<servers type="array">"""
|
||||
footer = "</servers></show-servers-response>"
|
||||
body = """<server>
|
||||
<name>%s</name>
|
||||
<display-name>%s</display-name>
|
||||
<iqn>%s</iqn>
|
||||
<target>iqn.2011-04.com.zadarastorage:vsa-xxx:1</target>
|
||||
<lun>0</lun>
|
||||
</server>"""
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if vol_name == vol:
|
||||
attachments = params['attachments']
|
||||
resp = header
|
||||
for server in attachments:
|
||||
srv_params = self._get_server_obj(server)
|
||||
resp += body % (server,
|
||||
srv_params['display-name'],
|
||||
srv_params['iqn'])
|
||||
resp += footer
|
||||
return resp
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _list_vol_snapshots(self):
|
||||
cg_name = self.url.split('/')[3]
|
||||
|
||||
header = """<show-snapshots-on-cg-response>
|
||||
<status type="integer">0</status>
|
||||
<snapshots type="array">"""
|
||||
footer = "</snapshots></show-snapshots-on-cg-response>"
|
||||
|
||||
body = """<snapshot>
|
||||
<name>%s</name>
|
||||
<display-name>%s</display-name>
|
||||
<status>normal</status>
|
||||
<cg-name>%s</cg-name>
|
||||
<pool-name>pool-00000001</pool-name>
|
||||
</snapshot>"""
|
||||
|
||||
for (vol_name, params) in RUNTIME_VARS['volumes']:
|
||||
if params['cg-name'] == cg_name:
|
||||
snapshots = params['snapshots']
|
||||
resp = header
|
||||
for snap in snapshots:
|
||||
resp += body % (snap, snap, cg_name)
|
||||
resp += footer
|
||||
return resp
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
|
||||
class FakeHTTPConnection(object):
|
||||
"""A fake http_client.HTTPConnection for zadara volume driver tests."""
|
||||
def __init__(self, host, port, use_ssl=False):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.use_ssl = use_ssl
|
||||
self.req = None
|
||||
|
||||
def request(self, method, url, body):
|
||||
self.req = FakeRequest(method, url, body)
|
||||
|
||||
def getresponse(self):
|
||||
return self.req
|
||||
|
||||
def close(self):
|
||||
self.req = None
|
||||
|
||||
|
||||
class FakeHTTPSConnection(FakeHTTPConnection):
|
||||
def __init__(self, host, port):
|
||||
super(FakeHTTPSConnection, self).__init__(host, port, use_ssl=True)
|
||||
|
||||
|
||||
class ZadaraVPSADriverTestCase(test.TestCase):
|
||||
"""Test case for Zadara VPSA volume driver."""
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def setUp(self):
|
||||
super(ZadaraVPSADriverTestCase, self).setUp()
|
||||
|
||||
global RUNTIME_VARS
|
||||
RUNTIME_VARS = copy.deepcopy(DEFAULT_RUNTIME_VARS)
|
||||
self.configuration = mock.Mock(conf.Configuration(None))
|
||||
self.configuration.append_config_values(zadara.zadara_opts)
|
||||
self.configuration.reserved_percentage = 10
|
||||
self.configuration.zadara_use_iser = True
|
||||
self.configuration.zadara_vpsa_host = '192.168.5.5'
|
||||
self.configuration.zadara_vpsa_port = '80'
|
||||
self.configuration.zadara_user = 'test'
|
||||
self.configuration.zadara_password = 'test_password'
|
||||
self.configuration.zadara_vpsa_poolname = 'pool-0001'
|
||||
self.configuration.zadara_vol_encrypt = False
|
||||
self.configuration.zadara_vol_name_template = 'OS_%s'
|
||||
self.configuration.zadara_vpsa_use_ssl = False
|
||||
self.configuration.zadara_default_snap_policy = False
|
||||
self.driver = (zadara.ZadaraVPSAISCSIDriver(
|
||||
configuration=self.configuration))
|
||||
self.driver.do_setup(None)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_create_destroy(self):
|
||||
"""Create/Delete volume."""
|
||||
volume = {'name': 'test_volume_01', 'size': 1}
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_create_destroy_multiple(self):
|
||||
"""Create/Delete multiple volumes."""
|
||||
self.driver.create_volume({'name': 'test_volume_01', 'size': 1})
|
||||
self.driver.create_volume({'name': 'test_volume_02', 'size': 2})
|
||||
self.driver.create_volume({'name': 'test_volume_03', 'size': 3})
|
||||
self.driver.delete_volume({'name': 'test_volume_02'})
|
||||
self.driver.delete_volume({'name': 'test_volume_03'})
|
||||
self.driver.delete_volume({'name': 'test_volume_01'})
|
||||
self.driver.delete_volume({'name': 'test_volume_04'})
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_destroy_non_existent(self):
|
||||
"""Delete non-existent volume."""
|
||||
volume = {'name': 'test_volume_02', 'size': 1}
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_empty_apis(self):
|
||||
"""Test empty func (for coverage only)."""
|
||||
context = None
|
||||
volume = {'name': 'test_volume_01', 'size': 1}
|
||||
self.driver.create_export(context, volume)
|
||||
self.driver.ensure_export(context, volume)
|
||||
self.driver.remove_export(context, volume)
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.driver.local_path,
|
||||
None)
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_volume_attach_detach(self):
|
||||
"""Test volume attachment and detach."""
|
||||
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
||||
connector = dict(initiator='test_iqn.1')
|
||||
self.driver.create_volume(volume)
|
||||
props = self.driver.initialize_connection(volume, connector)
|
||||
self.assertEqual('iser', props['driver_volume_type'])
|
||||
data = props['data']
|
||||
self.assertEqual('1.1.1.1:3260', data['target_portal'])
|
||||
self.assertEqual('iqn.2011-04.com.zadarastorage:vsa-xxx:1',
|
||||
data['target_iqn'])
|
||||
self.assertEqual(int('0'), data['target_lun'])
|
||||
self.assertEqual(123, data['volume_id'])
|
||||
self.assertEqual('CHAP', data['auth_method'])
|
||||
self.assertEqual('test_chap_user', data['auth_username'])
|
||||
self.assertEqual('test_chap_secret', data['auth_password'])
|
||||
self.driver.terminate_connection(volume, connector)
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
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(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_wrong_attach_params(self):
|
||||
"""Test different wrong attach scenarios."""
|
||||
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
||||
connector1 = dict(initiator='test_iqn.1')
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
self.driver.initialize_connection,
|
||||
volume1, connector1)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_wrong_detach_params(self):
|
||||
"""Test different wrong detachment scenarios."""
|
||||
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
||||
volume2 = {'name': 'test_volume_02', 'size': 1, 'id': 102}
|
||||
volume3 = {'name': 'test_volume_03', 'size': 1, 'id': 103}
|
||||
connector1 = dict(initiator='test_iqn.1')
|
||||
connector2 = dict(initiator='test_iqn.2')
|
||||
connector3 = dict(initiator='test_iqn.3')
|
||||
self.driver.create_volume(volume1)
|
||||
self.driver.create_volume(volume2)
|
||||
self.driver.initialize_connection(volume1, connector1)
|
||||
self.driver.initialize_connection(volume2, connector2)
|
||||
self.assertRaises(exception.ZadaraServerNotFound,
|
||||
self.driver.terminate_connection,
|
||||
volume1, connector3)
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
self.driver.terminate_connection,
|
||||
volume3, connector1)
|
||||
self.assertRaises(exception.FailedCmdWithDump,
|
||||
self.driver.terminate_connection,
|
||||
volume1, connector2)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_wrong_login_reply(self):
|
||||
"""Test wrong login reply."""
|
||||
|
||||
RUNTIME_VARS['login'] = """<hash>
|
||||
<access-key>%s</access-key>
|
||||
<status type="integer">0</status>
|
||||
</hash>"""
|
||||
self.assertRaises(exception.MalformedResponse,
|
||||
self.driver.do_setup, None)
|
||||
|
||||
RUNTIME_VARS['login'] = """
|
||||
<hash>
|
||||
<user>
|
||||
<updated-at type="datetime">2012-04-30...</updated-at>
|
||||
<id type="integer">1</id>
|
||||
<created-at type="datetime">2012-02-21...</created-at>
|
||||
<email>jsmith@example.com</email>
|
||||
<username>jsmith</username>
|
||||
</user>
|
||||
<access-key>%s</access-key>
|
||||
<status type="integer">0</status>
|
||||
</hash>"""
|
||||
self.assertRaises(exception.MalformedResponse,
|
||||
self.driver.do_setup, None)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_ssl_use(self):
|
||||
"""Coverage test for SSL connection."""
|
||||
self.flags(zadara_vpsa_use_ssl=True)
|
||||
self.driver.do_setup(None)
|
||||
self.flags(zadara_vpsa_use_ssl=False)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_bad_http_response(self):
|
||||
"""Coverage test for non-good HTTP response."""
|
||||
RUNTIME_VARS['status'] = 400
|
||||
|
||||
volume = {'name': 'test_volume_01', 'size': 1}
|
||||
self.assertRaises(exception.BadHTTPResponseStatus,
|
||||
self.driver.create_volume, volume)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_delete_without_detach(self):
|
||||
"""Test volume deletion without detach."""
|
||||
|
||||
volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
|
||||
connector1 = dict(initiator='test_iqn.1')
|
||||
connector2 = dict(initiator='test_iqn.2')
|
||||
connector3 = dict(initiator='test_iqn.3')
|
||||
|
||||
self.driver.create_volume(volume1)
|
||||
self.driver.initialize_connection(volume1, connector1)
|
||||
self.driver.initialize_connection(volume1, connector2)
|
||||
self.driver.initialize_connection(volume1, connector3)
|
||||
self.driver.delete_volume(volume1)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_no_active_ctrl(self):
|
||||
|
||||
RUNTIME_VARS['controllers'] = []
|
||||
volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
|
||||
connector = dict(initiator='test_iqn.1')
|
||||
self.driver.create_volume(volume)
|
||||
self.assertRaises(exception.ZadaraVPSANoActiveController,
|
||||
self.driver.initialize_connection,
|
||||
volume, connector)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_create_destroy_snapshot(self):
|
||||
"""Create/Delete snapshot test."""
|
||||
volume = {'name': 'test_volume_01', 'size': 1}
|
||||
snapshot = {'name': 'snap_01',
|
||||
'volume_name': volume['name']}
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
self.driver.create_snapshot,
|
||||
{'name': snapshot['name'],
|
||||
'volume_name': 'wrong_vol'})
|
||||
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
# Deleted should succeed for missing volume
|
||||
self.driver.delete_snapshot({'name': snapshot['name'],
|
||||
'volume_name': 'wrong_vol'})
|
||||
# Deleted should succeed for missing snap
|
||||
self.driver.delete_snapshot({'name': 'wrong_snap',
|
||||
'volume_name': volume['name']})
|
||||
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_expand_volume(self):
|
||||
"""Expand volume test."""
|
||||
volume = {'name': 'test_volume_01', 'size': 10}
|
||||
volume2 = {'name': 'test_volume_02', 'size': 10}
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
|
||||
self.assertRaises(exception.ZadaraVolumeNotFound,
|
||||
self.driver.extend_volume,
|
||||
volume2, 15)
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.driver.extend_volume,
|
||||
volume, 5)
|
||||
|
||||
self.driver.extend_volume(volume, 15)
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_create_destroy_clones(self):
|
||||
"""Create/Delete clones test."""
|
||||
volume1 = {'name': 'test_volume_01', 'id': '01', 'size': 1}
|
||||
volume2 = {'name': 'test_volume_02', 'id': '02', 'size': 2}
|
||||
volume3 = {'name': 'test_volume_03', 'id': '03', 'size': 1}
|
||||
snapshot = {'name': 'snap_01',
|
||||
'id': '01',
|
||||
'volume_name': volume1['name'],
|
||||
'volume_size': 1}
|
||||
|
||||
self.driver.create_volume(volume1)
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
# Test invalid vol reference
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
self.driver.create_volume_from_snapshot,
|
||||
volume2,
|
||||
{'name': snapshot['name'],
|
||||
'id': snapshot['id'],
|
||||
'volume_name': 'wrong_vol'})
|
||||
# Test invalid snap reference
|
||||
self.assertRaises(exception.SnapshotNotFound,
|
||||
self.driver.create_volume_from_snapshot,
|
||||
volume2,
|
||||
{'name': 'wrong_snap',
|
||||
'id': 'wrong_id',
|
||||
'volume_name': snapshot['volume_name']})
|
||||
# Test invalid src_vref for volume clone
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
self.driver.create_cloned_volume,
|
||||
volume3, volume2)
|
||||
self.driver.create_volume_from_snapshot(volume2, snapshot)
|
||||
self.driver.create_cloned_volume(volume3, volume1)
|
||||
self.driver.delete_volume(volume3)
|
||||
self.driver.delete_volume(volume2)
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
self.driver.delete_volume(volume1)
|
||||
|
||||
@mock.patch.object(http_client, 'HTTPConnection', FakeHTTPConnection)
|
||||
@mock.patch.object(http_client, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
def test_get_volume_stats(self):
|
||||
"""Get stats test."""
|
||||
self.configuration.safe_get.return_value = 'ZadaraVPSAISCSIDriver'
|
||||
data = self.driver.get_volume_stats(True)
|
||||
self.assertEqual('Zadara Storage', data['vendor_name'])
|
||||
self.assertEqual('unknown', data['total_capacity_gb'])
|
||||
self.assertEqual('unknown', data['free_capacity_gb'])
|
||||
self.assertEqual({'total_capacity_gb': 'unknown',
|
||||
'free_capacity_gb': 'unknown',
|
||||
'reserved_percentage':
|
||||
self.configuration.reserved_percentage,
|
||||
'QoS_support': False,
|
||||
'vendor_name': 'Zadara Storage',
|
||||
'driver_version': self.driver.VERSION,
|
||||
'storage_protocol': 'iSER',
|
||||
'volume_backend_name': 'ZadaraVPSAISCSIDriver'},
|
||||
data)
|
677
cinder/volume/drivers/zadara.py
Normal file
677
cinder/volume/drivers/zadara.py
Normal file
@ -0,0 +1,677 @@
|
||||
# Copyright (c) 2016 Zadara Storage, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
Volume driver for Zadara Virtual Private Storage Array (VPSA).
|
||||
|
||||
This driver requires VPSA with API version 15.07 or higher.
|
||||
"""
|
||||
|
||||
from lxml import etree
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from six.moves import http_client
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LE, _LW
|
||||
from cinder import interface
|
||||
from cinder.volume import driver
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
zadara_opts = [
|
||||
cfg.BoolOpt('zadara_use_iser',
|
||||
default=True,
|
||||
help='VPSA - Use ISER instead of iSCSI'),
|
||||
cfg.StrOpt('zadara_vpsa_host',
|
||||
default=None,
|
||||
help='VPSA - Management Host name or IP address'),
|
||||
cfg.PortOpt('zadara_vpsa_port',
|
||||
default=None,
|
||||
help='VPSA - Port number'),
|
||||
cfg.BoolOpt('zadara_vpsa_use_ssl',
|
||||
default=False,
|
||||
help='VPSA - Use SSL connection'),
|
||||
cfg.StrOpt('zadara_user',
|
||||
default=None,
|
||||
help='VPSA - Username'),
|
||||
cfg.StrOpt('zadara_password',
|
||||
default=None,
|
||||
help='VPSA - Password',
|
||||
secret=True),
|
||||
cfg.StrOpt('zadara_vpsa_poolname',
|
||||
default=None,
|
||||
help='VPSA - Storage Pool assigned for volumes'),
|
||||
cfg.BoolOpt('zadara_vol_encrypt',
|
||||
default=False,
|
||||
help='VPSA - Default encryption policy for volumes'),
|
||||
cfg.StrOpt('zadara_vol_name_template',
|
||||
default='OS_%s',
|
||||
help='VPSA - Default template for VPSA volume names'),
|
||||
cfg.BoolOpt('zadara_default_snap_policy',
|
||||
default=False,
|
||||
help="VPSA - Attach snapshot policy for volumes")]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(zadara_opts)
|
||||
|
||||
|
||||
class ZadaraVPSAConnection(object):
|
||||
"""Executes volume driver commands on VPSA."""
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.access_key = None
|
||||
|
||||
self.ensure_connection()
|
||||
|
||||
def _generate_vpsa_cmd(self, cmd, **kwargs):
|
||||
"""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:
|
||||
# 'command': (method, API_URL, {optional parameters})
|
||||
vpsa_commands = {
|
||||
'login': ('POST',
|
||||
'/api/users/login.xml',
|
||||
{'user': self.conf.zadara_user,
|
||||
'password': self.conf.zadara_password}),
|
||||
|
||||
# Volume operations
|
||||
'create_volume': ('POST',
|
||||
'/api/volumes.xml',
|
||||
{'name': kwargs.get('name'),
|
||||
'capacity': kwargs.get('size'),
|
||||
'pool': self.conf.zadara_vpsa_poolname,
|
||||
'thin': 'YES',
|
||||
'crypt': 'YES'
|
||||
if self.conf.zadara_vol_encrypt else 'NO',
|
||||
'attachpolicies': 'NO'
|
||||
if not self.conf.zadara_default_snap_policy
|
||||
else 'YES'}),
|
||||
'delete_volume': ('DELETE',
|
||||
'/api/volumes/%s.xml' % kwargs.get('vpsa_vol'),
|
||||
{'force': 'YES'}),
|
||||
'expand_volume': ('POST',
|
||||
'/api/volumes/%s/expand.xml'
|
||||
% kwargs.get('vpsa_vol'),
|
||||
{'capacity': kwargs.get('size')}),
|
||||
|
||||
# Snapshot operations
|
||||
# Snapshot request is triggered for a single volume though the
|
||||
# API call implies that snapshot is triggered for CG (legacy API).
|
||||
'create_snapshot': ('POST',
|
||||
'/api/consistency_groups/%s/snapshots.xml'
|
||||
% kwargs.get('cg_name'),
|
||||
{'display_name': kwargs.get('snap_name')}),
|
||||
'delete_snapshot': ('DELETE',
|
||||
'/api/snapshots/%s.xml'
|
||||
% kwargs.get('snap_id'),
|
||||
{}),
|
||||
|
||||
'create_clone_from_snap': ('POST',
|
||||
'/api/consistency_groups/%s/clone.xml'
|
||||
% kwargs.get('cg_name'),
|
||||
{'name': kwargs.get('name'),
|
||||
'snapshot': kwargs.get('snap_id')}),
|
||||
|
||||
'create_clone': ('POST',
|
||||
'/api/consistency_groups/%s/clone.xml'
|
||||
% kwargs.get('cg_name'),
|
||||
{'name': kwargs.get('name')}),
|
||||
|
||||
# Server operations
|
||||
'create_server': ('POST',
|
||||
'/api/servers.xml',
|
||||
{'display_name': kwargs.get('initiator'),
|
||||
'iqn': kwargs.get('initiator')}),
|
||||
|
||||
# Attach/Detach operations
|
||||
'attach_volume': ('POST',
|
||||
'/api/servers/%s/volumes.xml'
|
||||
% kwargs.get('vpsa_srv'),
|
||||
{'volume_name[]': kwargs.get('vpsa_vol'),
|
||||
'force': 'NO'}),
|
||||
'detach_volume': ('POST',
|
||||
'/api/volumes/%s/detach.xml'
|
||||
% kwargs.get('vpsa_vol'),
|
||||
{'server_name[]': kwargs.get('vpsa_srv'),
|
||||
'force': 'NO'}),
|
||||
|
||||
# Get operations
|
||||
'list_volumes': ('GET',
|
||||
'/api/volumes.xml',
|
||||
{}),
|
||||
'list_pools': ('GET',
|
||||
'/api/pools.xml',
|
||||
{}),
|
||||
'list_controllers': ('GET',
|
||||
'/api/vcontrollers.xml',
|
||||
{}),
|
||||
'list_servers': ('GET',
|
||||
'/api/servers.xml',
|
||||
{}),
|
||||
'list_vol_attachments': ('GET',
|
||||
'/api/volumes/%s/servers.xml'
|
||||
% kwargs.get('vpsa_vol'),
|
||||
{}),
|
||||
'list_vol_snapshots': ('GET',
|
||||
'/api/consistency_groups/%s/snapshots.xml'
|
||||
% kwargs.get('cg_name'),
|
||||
{})}
|
||||
|
||||
if cmd not in vpsa_commands:
|
||||
raise exception.UnknownCmd(cmd=cmd)
|
||||
else:
|
||||
(method, url, params) = vpsa_commands[cmd]
|
||||
|
||||
if method == 'GET':
|
||||
# For GET commands add parameters to the URL
|
||||
params.update(dict(access_key=self.access_key,
|
||||
page=1, start=0, limit=0))
|
||||
url += '?' + _joined_params(params)
|
||||
body = ''
|
||||
|
||||
elif method == 'DELETE':
|
||||
# For DELETE commands add parameters to the URL
|
||||
params.update(dict(access_key=self.access_key))
|
||||
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:
|
||||
msg = (_('Method %(method)s is not defined') %
|
||||
{'method': method})
|
||||
LOG.error(msg)
|
||||
raise AssertionError(msg)
|
||||
|
||||
return (method, url, body)
|
||||
|
||||
def ensure_connection(self, cmd=None):
|
||||
"""Retrieve access key for VPSA connection."""
|
||||
|
||||
if self.access_key or cmd == 'login':
|
||||
return
|
||||
|
||||
cmd = 'login'
|
||||
xml_tree = self.send_cmd(cmd)
|
||||
user = xml_tree.find('user')
|
||||
if user is None:
|
||||
raise (exception.MalformedResponse(cmd=cmd,
|
||||
reason=_('no "user" field')))
|
||||
access_key = user.findtext('access-key')
|
||||
if access_key is None:
|
||||
raise (exception.MalformedResponse(cmd=cmd,
|
||||
reason=_('no "access-key" field')))
|
||||
self.access_key = access_key
|
||||
|
||||
def send_cmd(self, cmd, **kwargs):
|
||||
"""Send command to VPSA Controller."""
|
||||
|
||||
self.ensure_connection(cmd)
|
||||
|
||||
(method, url, body) = self._generate_vpsa_cmd(cmd, **kwargs)
|
||||
LOG.debug('Invoking %(cmd)s using %(method)s request.',
|
||||
{'cmd': cmd, 'method': method})
|
||||
|
||||
if self.conf.zadara_vpsa_use_ssl:
|
||||
connection = (http_client.HTTPSConnection(
|
||||
self.conf.zadara_vpsa_host,
|
||||
self.conf.zadara_vpsa_port))
|
||||
else:
|
||||
connection = http_client.HTTPConnection(self.conf.zadara_vpsa_host,
|
||||
self.conf.zadara_vpsa_port)
|
||||
connection.request(method, url, body)
|
||||
response = connection.getresponse()
|
||||
|
||||
if response.status != 200:
|
||||
connection.close()
|
||||
raise exception.BadHTTPResponseStatus(status=response.status)
|
||||
data = response.read()
|
||||
connection.close()
|
||||
|
||||
xml_tree = etree.fromstring(data)
|
||||
status = xml_tree.findtext('status')
|
||||
if status != '0':
|
||||
raise exception.FailedCmdWithDump(status=status, data=data)
|
||||
|
||||
if method in ['POST', 'DELETE']:
|
||||
LOG.debug('Operation completed with status code %(status)s',
|
||||
{'status': status})
|
||||
return xml_tree
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
"""Zadara VPSA iSCSI/iSER volume driver."""
|
||||
|
||||
VERSION = '15.07'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self.configuration.append_config_values(zadara_opts)
|
||||
|
||||
def do_setup(self, context):
|
||||
"""Any initialization the volume driver does while starting.
|
||||
|
||||
Establishes initial connection with VPSA and retrieves access_key.
|
||||
"""
|
||||
self.vpsa = ZadaraVPSAConnection(self.configuration)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Returns an error (exception) if prerequisites aren't met."""
|
||||
self.vpsa.ensure_connection()
|
||||
|
||||
def local_path(self, volume):
|
||||
"""Return local path to existing local volume."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _xml_parse_helper(self, xml_tree, first_level, search_tuple,
|
||||
first=True):
|
||||
"""Helper for parsing VPSA's XML output.
|
||||
|
||||
Returns single item if first==True or list for multiple selection.
|
||||
If second argument in search_tuple is None - returns all items with
|
||||
appropriate key.
|
||||
"""
|
||||
|
||||
objects = xml_tree.find(first_level)
|
||||
if objects is None:
|
||||
return None
|
||||
|
||||
result_list = []
|
||||
(key, value) = search_tuple
|
||||
for object in objects.getchildren():
|
||||
found_value = object.findtext(key)
|
||||
if found_value and (found_value == value or value is None):
|
||||
if first:
|
||||
return object
|
||||
else:
|
||||
result_list.append(object)
|
||||
return result_list if result_list else None
|
||||
|
||||
def _get_vpsa_volume_name_and_size(self, name):
|
||||
"""Return VPSA's name & size for the volume."""
|
||||
xml_tree = self.vpsa.send_cmd('list_volumes')
|
||||
volume = self._xml_parse_helper(xml_tree, 'volumes',
|
||||
('display-name', name))
|
||||
if volume is not None:
|
||||
return (volume.findtext('name'),
|
||||
int(volume.findtext('virtual-capacity')))
|
||||
|
||||
return (None, None)
|
||||
|
||||
def _get_vpsa_volume_name(self, name):
|
||||
"""Return VPSA's name for the volume."""
|
||||
(vol_name, size) = self._get_vpsa_volume_name_and_size(name)
|
||||
return vol_name
|
||||
|
||||
def _get_volume_cg_name(self, name):
|
||||
"""Return name of the consistency group for the volume.
|
||||
|
||||
cg-name is a volume uniqe identifier (legacy attribute)
|
||||
and not consistency group as it may imply.
|
||||
"""
|
||||
xml_tree = self.vpsa.send_cmd('list_volumes')
|
||||
volume = self._xml_parse_helper(xml_tree, 'volumes',
|
||||
('display-name', name))
|
||||
if volume is not None:
|
||||
return volume.findtext('cg-name')
|
||||
|
||||
return None
|
||||
|
||||
def _get_snap_id(self, cg_name, snap_name):
|
||||
"""Return snapshot ID for particular volume."""
|
||||
xml_tree = self.vpsa.send_cmd('list_vol_snapshots',
|
||||
cg_name=cg_name)
|
||||
snap = self._xml_parse_helper(xml_tree, 'snapshots',
|
||||
('display-name', snap_name))
|
||||
if snap is not None:
|
||||
return snap.findtext('name')
|
||||
|
||||
return None
|
||||
|
||||
def _get_pool_capacity(self, pool_name):
|
||||
"""Return pool's total and available capacities."""
|
||||
xml_tree = self.vpsa.send_cmd('list_pools')
|
||||
pool = self._xml_parse_helper(xml_tree, 'pools',
|
||||
('name', pool_name))
|
||||
if pool is not None:
|
||||
total = int(pool.findtext('capacity'))
|
||||
free = int(float(pool.findtext('available-capacity')))
|
||||
LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
|
||||
{'name': pool_name, 'total': total, 'free': free})
|
||||
return (total, free)
|
||||
|
||||
return ('unknown', 'unknown')
|
||||
|
||||
def _get_active_controller_details(self):
|
||||
"""Return details of VPSA's active controller."""
|
||||
xml_tree = self.vpsa.send_cmd('list_controllers')
|
||||
ctrl = self._xml_parse_helper(xml_tree, 'vcontrollers',
|
||||
('state', 'active'))
|
||||
if ctrl is not None:
|
||||
return dict(target=ctrl.findtext('target'),
|
||||
ip=ctrl.findtext('iscsi-ip'),
|
||||
chap_user=ctrl.findtext('vpsa-chap-user'),
|
||||
chap_passwd=ctrl.findtext('vpsa-chap-secret'))
|
||||
return None
|
||||
|
||||
def _get_server_name(self, initiator):
|
||||
"""Return VPSA's name for server object with given IQN."""
|
||||
xml_tree = self.vpsa.send_cmd('list_servers')
|
||||
server = self._xml_parse_helper(xml_tree, 'servers',
|
||||
('iqn', initiator))
|
||||
if server is not None:
|
||||
return server.findtext('name')
|
||||
return None
|
||||
|
||||
def _create_vpsa_server(self, initiator):
|
||||
"""Create server object within VPSA (if doesn't exist)."""
|
||||
vpsa_srv = self._get_server_name(initiator)
|
||||
if not vpsa_srv:
|
||||
xml_tree = self.vpsa.send_cmd('create_server', initiator=initiator)
|
||||
vpsa_srv = xml_tree.findtext('server-name')
|
||||
return vpsa_srv
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Create volume."""
|
||||
self.vpsa.send_cmd(
|
||||
'create_volume',
|
||||
name=self.configuration.zadara_vol_name_template % volume['name'],
|
||||
size=volume['size'])
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Delete volume.
|
||||
|
||||
Return ok if doesn't exist. Auto detach from all servers.
|
||||
"""
|
||||
# Get volume name
|
||||
name = self.configuration.zadara_vol_name_template % volume['name']
|
||||
vpsa_vol = self._get_vpsa_volume_name(name)
|
||||
if not vpsa_vol:
|
||||
LOG.warning(_LW('Volume %s could not be found. '
|
||||
'It might be already deleted'), name)
|
||||
return
|
||||
|
||||
# Check attachment info and detach from all
|
||||
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
||||
vpsa_vol=vpsa_vol)
|
||||
servers = self._xml_parse_helper(xml_tree, 'servers',
|
||||
('iqn', None), first=False)
|
||||
if servers:
|
||||
for server in servers:
|
||||
vpsa_srv = server.findtext('name')
|
||||
if vpsa_srv:
|
||||
self.vpsa.send_cmd('detach_volume',
|
||||
vpsa_srv=vpsa_srv,
|
||||
vpsa_vol=vpsa_vol)
|
||||
|
||||
# Delete volume
|
||||
self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot."""
|
||||
|
||||
LOG.debug('Create snapshot: %s', snapshot['name'])
|
||||
|
||||
# Retrieve the CG name for the base volume
|
||||
volume_name = (self.configuration.zadara_vol_name_template
|
||||
% snapshot['volume_name'])
|
||||
cg_name = self._get_volume_cg_name(volume_name)
|
||||
if not cg_name:
|
||||
msg = _('Volume %(name)s not found') % {'name': volume_name}
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
|
||||
self.vpsa.send_cmd('create_snapshot',
|
||||
cg_name=cg_name,
|
||||
snap_name=snapshot['name'])
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot."""
|
||||
|
||||
LOG.debug('Delete snapshot: %s', snapshot['name'])
|
||||
|
||||
# Retrieve the CG name for the base volume
|
||||
volume_name = (self.configuration.zadara_vol_name_template
|
||||
% snapshot['volume_name'])
|
||||
cg_name = self._get_volume_cg_name(volume_name)
|
||||
if not cg_name:
|
||||
# If the volume isn't present, then don't attempt to delete
|
||||
LOG.warning(_LW('snapshot: original volume %s not found, '
|
||||
'skipping delete operation'),
|
||||
volume_name)
|
||||
return
|
||||
|
||||
snap_id = self._get_snap_id(cg_name, snapshot['name'])
|
||||
if not snap_id:
|
||||
# If the snapshot isn't present, then don't attempt to delete
|
||||
LOG.warning(_LW('snapshot: snapshot %s not found, '
|
||||
'skipping delete operation'), snapshot['name'])
|
||||
return
|
||||
|
||||
self.vpsa.send_cmd('delete_snapshot',
|
||||
snap_id=snap_id)
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Creates a volume from a snapshot."""
|
||||
|
||||
LOG.debug('Creating volume from snapshot: %s', snapshot['name'])
|
||||
|
||||
# Retrieve the CG name for the base volume
|
||||
volume_name = (self.configuration.zadara_vol_name_template
|
||||
% snapshot['volume_name'])
|
||||
cg_name = self._get_volume_cg_name(volume_name)
|
||||
if not cg_name:
|
||||
LOG.error(_LE('Volume %(name)s not found'), {'name': volume_name})
|
||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||
|
||||
snap_id = self._get_snap_id(cg_name, snapshot['name'])
|
||||
if not snap_id:
|
||||
LOG.error(_LE('Snapshot %(name)s not found'),
|
||||
{'name': snapshot['name']})
|
||||
raise exception.SnapshotNotFound(snapshot_id=snapshot['id'])
|
||||
|
||||
self.vpsa.send_cmd('create_clone_from_snap',
|
||||
cg_name=cg_name,
|
||||
name=self.configuration.zadara_vol_name_template
|
||||
% volume['name'],
|
||||
snap_id=snap_id)
|
||||
|
||||
if (volume['size'] > snapshot['volume_size']):
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a clone of the specified volume."""
|
||||
|
||||
LOG.debug('Creating clone of volume: %s', src_vref['name'])
|
||||
|
||||
# Retrieve the CG name for the base volume
|
||||
volume_name = (self.configuration.zadara_vol_name_template
|
||||
% src_vref['name'])
|
||||
cg_name = self._get_volume_cg_name(volume_name)
|
||||
if not cg_name:
|
||||
LOG.error(_LE('Volume %(name)s not found'), {'name': volume_name})
|
||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||
|
||||
self.vpsa.send_cmd('create_clone',
|
||||
cg_name=cg_name,
|
||||
name=self.configuration.zadara_vol_name_template
|
||||
% volume['name'])
|
||||
|
||||
if (volume['size'] > src_vref['size']):
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
"""Extend an existing volume."""
|
||||
# Get volume name
|
||||
name = self.configuration.zadara_vol_name_template % volume['name']
|
||||
(vpsa_vol, size) = self._get_vpsa_volume_name_and_size(name)
|
||||
if not vpsa_vol:
|
||||
msg = (_('Volume %(name)s could not be found. '
|
||||
'It might be already deleted') % {'name': name})
|
||||
LOG.error(msg)
|
||||
raise exception.ZadaraVolumeNotFound(reason=msg)
|
||||
|
||||
if new_size < size:
|
||||
raise exception.InvalidInput(
|
||||
reason=_('%(new_size)s < current size %(size)s') %
|
||||
{'new_size': new_size, 'size': size})
|
||||
|
||||
expand_size = new_size - size
|
||||
self.vpsa.send_cmd('expand_volume',
|
||||
vpsa_vol=vpsa_vol,
|
||||
size=expand_size)
|
||||
|
||||
def create_export(self, context, volume, vg=None):
|
||||
"""Irrelevant for VPSA volumes. Export created during attachment."""
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""Irrelevant for VPSA volumes. Export created during attachment."""
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Irrelevant for VPSA volumes. Export removed during detach."""
|
||||
pass
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Attach volume to initiator/host.
|
||||
|
||||
During this call VPSA exposes volume to particular Initiator. It also
|
||||
creates a 'server' entity for Initiator (if it was not created before)
|
||||
|
||||
All necessary connection information is returned, including auth data.
|
||||
Connection data (target, LUN) is not stored in the DB.
|
||||
"""
|
||||
|
||||
# Get/Create server name for IQN
|
||||
initiator_name = connector['initiator']
|
||||
vpsa_srv = self._create_vpsa_server(initiator_name)
|
||||
if not vpsa_srv:
|
||||
raise exception.ZadaraServerCreateFailure(name=initiator_name)
|
||||
|
||||
# Get volume name
|
||||
name = self.configuration.zadara_vol_name_template % volume['name']
|
||||
vpsa_vol = self._get_vpsa_volume_name(name)
|
||||
if not vpsa_vol:
|
||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||
|
||||
# Get Active controller details
|
||||
ctrl = self._get_active_controller_details()
|
||||
if not ctrl:
|
||||
raise exception.ZadaraVPSANoActiveController()
|
||||
|
||||
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
||||
vpsa_vol=vpsa_vol)
|
||||
attach = self._xml_parse_helper(xml_tree, 'servers',
|
||||
('name', vpsa_srv))
|
||||
# Attach volume to server
|
||||
if attach is None:
|
||||
self.vpsa.send_cmd('attach_volume',
|
||||
vpsa_srv=vpsa_srv,
|
||||
vpsa_vol=vpsa_vol)
|
||||
# Get connection info
|
||||
xml_tree = self.vpsa.send_cmd('list_vol_attachments',
|
||||
vpsa_vol=vpsa_vol)
|
||||
server = self._xml_parse_helper(xml_tree, 'servers',
|
||||
('iqn', initiator_name))
|
||||
if server is None:
|
||||
raise exception.ZadaraAttachmentsNotFound(name=name)
|
||||
target = server.findtext('target')
|
||||
lun = int(server.findtext('lun'))
|
||||
if target is None or lun is None:
|
||||
raise exception.ZadaraInvalidAttachmentInfo(
|
||||
name=name,
|
||||
reason=_('target=%(target)s, lun=%(lun)s') %
|
||||
{'target': target, 'lun': lun})
|
||||
|
||||
properties = {}
|
||||
properties['target_discovered'] = False
|
||||
properties['target_portal'] = '%s:%s' % (ctrl['ip'], '3260')
|
||||
properties['target_iqn'] = target
|
||||
properties['target_lun'] = lun
|
||||
properties['volume_id'] = volume['id']
|
||||
properties['auth_method'] = 'CHAP'
|
||||
properties['auth_username'] = ctrl['chap_user']
|
||||
properties['auth_password'] = ctrl['chap_passwd']
|
||||
|
||||
LOG.debug('Attach properties: %(properties)s',
|
||||
{'properties': properties})
|
||||
return {'driver_volume_type':
|
||||
('iser' if (self.configuration.safe_get('zadara_use_iser'))
|
||||
else 'iscsi'), 'data': properties}
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Detach volume from the initiator."""
|
||||
# Get server name for IQN
|
||||
initiator_name = connector['initiator']
|
||||
vpsa_srv = self._get_server_name(initiator_name)
|
||||
if not vpsa_srv:
|
||||
raise exception.ZadaraServerNotFound(name=initiator_name)
|
||||
|
||||
# Get volume name
|
||||
name = self.configuration.zadara_vol_name_template % volume['name']
|
||||
vpsa_vol = self._get_vpsa_volume_name(name)
|
||||
if not vpsa_vol:
|
||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||
|
||||
# Detach volume from server
|
||||
self.vpsa.send_cmd('detach_volume',
|
||||
vpsa_srv=vpsa_srv,
|
||||
vpsa_vol=vpsa_vol)
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume stats.
|
||||
|
||||
If 'refresh' is True, run update the stats first.
|
||||
"""
|
||||
|
||||
if refresh:
|
||||
self._update_volume_stats()
|
||||
|
||||
return self._stats
|
||||
|
||||
def _update_volume_stats(self):
|
||||
"""Retrieve stats info from volume group."""
|
||||
|
||||
LOG.debug("Updating volume stats")
|
||||
data = {}
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
storage_protocol = ('iSER' if
|
||||
(self.configuration.safe_get('zadara_use_iser'))
|
||||
else 'iSCSI')
|
||||
data["volume_backend_name"] = backend_name or self.__class__.__name__
|
||||
data["vendor_name"] = 'Zadara Storage'
|
||||
data["driver_version"] = self.VERSION
|
||||
data["storage_protocol"] = storage_protocol
|
||||
data['reserved_percentage'] = self.configuration.reserved_percentage
|
||||
data['QoS_support'] = False
|
||||
|
||||
(total, free) = self._get_pool_capacity(self.configuration.
|
||||
zadara_vpsa_poolname)
|
||||
data['total_capacity_gb'] = total
|
||||
data['free_capacity_gb'] = free
|
||||
|
||||
self._stats = data
|
3
releasenotes/notes/ZadaraStorage-13a5fff6f4fa1710.yaml
Normal file
3
releasenotes/notes/ZadaraStorage-13a5fff6f4fa1710.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added volume driver for Zadara Storage VPSA.
|
Loading…
Reference in New Issue
Block a user