Add backend driver for Zadara Storage VPSA
Zadara Storage VPSA Cinder ISCSI backend driver This drivers address support of Zadara Storage VPSA as Cinder backend driver. It implements the minimum set of features required. DocImpact Implements: blueprint zadara-storage-vpsa-driver Change-Id: I0ae4b1e6259c36503114ed40999c5ea4a6be4c5f
This commit is contained in:
parent
78ad6fbde1
commit
a85522cc3f
@ -1009,6 +1009,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, "
|
||||
|
@ -157,6 +157,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 \
|
||||
@ -331,6 +332,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