Add features to Zadara Storage Cinder driver
- move to Zadara APIs 13.07 - added support for extend volume - added support for create/delete snapshot - added support for create clones from volumes and snaps - added support for multi-backend - added volume stats - added tests Implements: blueprint zadara-cinder-driver-update Change-Id: Iad5908a50980c59df2d8d4702743a0b99f82f9b7
This commit is contained in:
@@ -21,13 +21,14 @@ Tests for Zadara VPSA volume driver
|
||||
|
||||
import copy
|
||||
import httplib
|
||||
import mox
|
||||
|
||||
from cinder import exception
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import test
|
||||
from cinder.volume.drivers import zadara
|
||||
|
||||
from lxml import etree
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.zadara import zadara_opts
|
||||
from cinder.volume.drivers.zadara import ZadaraVPSAISCSIDriver
|
||||
|
||||
LOG = logging.getLogger("cinder.volume.driver")
|
||||
|
||||
@@ -38,7 +39,7 @@ DEFAULT_RUNTIME_VARS = {
|
||||
'access_key': '0123456789ABCDEF',
|
||||
'volumes': [],
|
||||
'servers': [],
|
||||
'controllers': [('active_ctrl', {'display_name': 'test_ctrl'})],
|
||||
'controllers': [('active_ctrl', {'display-name': 'test_ctrl'})],
|
||||
'counter': 1000,
|
||||
|
||||
'login': """
|
||||
@@ -99,11 +100,20 @@ class FakeRequest(object):
|
||||
('/api/volumes.xml', self._create_volume),
|
||||
('/api/servers.xml', self._create_server),
|
||||
('/api/servers/*/volumes.xml', self._attach),
|
||||
('/api/volumes/*/detach.xml', self._detach)],
|
||||
'DELETE': [('/api/volumes/*', self._delete)],
|
||||
('/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)]
|
||||
}
|
||||
@@ -156,6 +166,9 @@ class FakeRequest(object):
|
||||
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))
|
||||
@@ -166,6 +179,7 @@ class FakeRequest(object):
|
||||
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
|
||||
@@ -209,6 +223,65 @@ class FakeRequest(object):
|
||||
|
||||
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]
|
||||
|
||||
@@ -223,10 +296,16 @@ class FakeRequest(object):
|
||||
|
||||
return RUNTIME_VARS['bad_volume']
|
||||
|
||||
def _generate_list_resp(self, header, footer, body, lst):
|
||||
def _generate_list_resp(self, header, footer, body, lst, vol):
|
||||
resp = header
|
||||
for (obj, params) in lst:
|
||||
resp += body % (obj, params['display_name'])
|
||||
if vol:
|
||||
resp += body % (obj,
|
||||
params['display-name'],
|
||||
params['cg-name'],
|
||||
params['capacity'])
|
||||
else:
|
||||
resp += body % (obj, params['display-name'])
|
||||
resp += footer
|
||||
return resp
|
||||
|
||||
@@ -238,8 +317,9 @@ class FakeRequest(object):
|
||||
body = """<volume>
|
||||
<name>%s</name>
|
||||
<display-name>%s</display-name>
|
||||
<cg-name>%s</cg-name>
|
||||
<status>Available</status>
|
||||
<virtual-capacity type='integer'>1</virtual-capacity>
|
||||
<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>
|
||||
@@ -249,7 +329,8 @@ class FakeRequest(object):
|
||||
return self._generate_list_resp(header,
|
||||
footer,
|
||||
body,
|
||||
RUNTIME_VARS['volumes'])
|
||||
RUNTIME_VARS['volumes'],
|
||||
True)
|
||||
|
||||
def _list_controllers(self):
|
||||
header = """<show-vcontrollers-response>
|
||||
@@ -272,7 +353,16 @@ class FakeRequest(object):
|
||||
return self._generate_list_resp(header,
|
||||
footer,
|
||||
body,
|
||||
RUNTIME_VARS['controllers'])
|
||||
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>
|
||||
@@ -290,7 +380,7 @@ class FakeRequest(object):
|
||||
|
||||
resp = header
|
||||
for (obj, params) in RUNTIME_VARS['servers']:
|
||||
resp += body % (obj, params['display_name'], params['iqn'])
|
||||
resp += body % (obj, params['display-name'], params['iqn'])
|
||||
resp += footer
|
||||
return resp
|
||||
|
||||
@@ -321,13 +411,40 @@ class FakeRequest(object):
|
||||
for server in attachments:
|
||||
srv_params = self._get_server_obj(server)
|
||||
resp += body % (server,
|
||||
srv_params['display_name'],
|
||||
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 httplib.HTTPConnection for zadara volume driver tests."""
|
||||
@@ -363,14 +480,18 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
LOG.debug('Enter: setUp')
|
||||
super(ZadaraVPSADriverTestCase, self).setUp()
|
||||
self.flags(
|
||||
zadara_user='test',
|
||||
zadara_password='test_password',
|
||||
)
|
||||
|
||||
global RUNTIME_VARS
|
||||
RUNTIME_VARS = copy.deepcopy(DEFAULT_RUNTIME_VARS)
|
||||
|
||||
self.driver = zadara.ZadaraVPSAISCSIDriver()
|
||||
self.configuration = conf.Configuration(None)
|
||||
self.configuration.append_config_values(zadara_opts)
|
||||
self.configuration.reserved_percentage = 10
|
||||
self.configuration.zadara_user = 'test'
|
||||
self.configuration.zadara_password = 'test_password'
|
||||
self.configuration.zadara_vpsa_poolname = 'pool-0001'
|
||||
|
||||
self.driver = ZadaraVPSAISCSIDriver(configuration=self.configuration)
|
||||
self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection)
|
||||
self.stubs.Set(httplib, 'HTTPSConnection', FakeHTTPSConnection)
|
||||
self.driver.do_setup(None)
|
||||
@@ -417,15 +538,6 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
||||
self.driver.ensure_export(context, volume)
|
||||
self.driver.remove_export(context, volume)
|
||||
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.driver.create_volume_from_snapshot,
|
||||
volume, None)
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.driver.create_snapshot,
|
||||
None)
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.driver.delete_snapshot,
|
||||
None)
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.driver.local_path,
|
||||
None)
|
||||
@@ -579,3 +691,107 @@ class ZadaraVPSADriverTestCase(test.TestCase):
|
||||
self.assertRaises(exception.ZadaraVPSANoActiveController,
|
||||
self.driver.initialize_connection,
|
||||
volume, connector)
|
||||
|
||||
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.VolumeNotFound,
|
||||
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)
|
||||
|
||||
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.VolumeNotFound,
|
||||
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)
|
||||
|
||||
def test_create_destroy_clones(self):
|
||||
"""Create/Delete clones test."""
|
||||
volume1 = {'name': 'test_volume_01', 'size': 1}
|
||||
volume2 = {'name': 'test_volume_02', 'size': 1}
|
||||
volume3 = {'name': 'test_volume_03', 'size': 1}
|
||||
snapshot = {'name': 'snap_01',
|
||||
'volume_name': volume1['name']}
|
||||
|
||||
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'],
|
||||
'volume_name': 'wrong_vol'})
|
||||
# Test invalid snap reference
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
self.driver.create_volume_from_snapshot,
|
||||
volume2,
|
||||
{'name': 'wrong_snap',
|
||||
'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)
|
||||
|
||||
def test_get_volume_stats(self):
|
||||
"""Get stats test."""
|
||||
|
||||
self.mox.StubOutWithMock(self.configuration, 'safe_get')
|
||||
self.configuration.safe_get('volume_backend_name'). \
|
||||
AndReturn('ZadaraVPSAISCSIDriver')
|
||||
self.mox.ReplayAll()
|
||||
|
||||
data = self.driver.get_volume_stats(True)
|
||||
|
||||
self.assertEqual(data['vendor_name'], 'Zadara Storage')
|
||||
self.assertEqual(data['total_capacity_gb'], 'infinite')
|
||||
self.assertEqual(data['free_capacity_gb'], 'infinite')
|
||||
|
||||
self.assertEquals(data,
|
||||
{'total_capacity_gb': 'infinite',
|
||||
'free_capacity_gb': 'infinite',
|
||||
'reserved_percentage':
|
||||
self.configuration.reserved_percentage,
|
||||
'QoS_support': False,
|
||||
'vendor_name': 'Zadara Storage',
|
||||
'driver_version': self.driver.VERSION,
|
||||
'storage_protocol': 'iSCSI',
|
||||
'volume_backend_name': 'ZadaraVPSAISCSIDriver',
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"""
|
||||
Volume driver for Zadara Virtual Private Storage Array (VPSA).
|
||||
|
||||
This driver requires VPSA with API ver.12.06 or higher.
|
||||
This driver requires VPSA with API ver.13.07 or higher.
|
||||
"""
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@ from cinder import exception
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder.volume import driver
|
||||
|
||||
|
||||
LOG = logging.getLogger("cinder.volume.driver")
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
zadara_opts = [
|
||||
cfg.StrOpt('zadara_vpsa_ip',
|
||||
@@ -56,12 +55,12 @@ zadara_opts = [
|
||||
default=None,
|
||||
help='Name of VPSA storage pool for volumes'),
|
||||
|
||||
cfg.StrOpt('zadara_default_cache_policy',
|
||||
default='write-through',
|
||||
help='Default cache policy for volumes'),
|
||||
cfg.StrOpt('zadara_default_encryption',
|
||||
default='NO',
|
||||
help='Default encryption policy for volumes'),
|
||||
cfg.BoolOpt('zadara_vol_thin',
|
||||
default=True,
|
||||
help='Default thin provisioning policy for volumes'),
|
||||
cfg.BoolOpt('zadara_vol_encrypt',
|
||||
default=False,
|
||||
help='Default encryption policy for volumes'),
|
||||
cfg.StrOpt('zadara_default_striping_mode',
|
||||
default='simple',
|
||||
help='Default striping mode for volumes'),
|
||||
@@ -85,12 +84,8 @@ CONF.register_opts(zadara_opts)
|
||||
class ZadaraVPSAConnection(object):
|
||||
"""Executes volume driver commands on VPSA."""
|
||||
|
||||
def __init__(self, host, port, ssl, user, password):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.use_ssl = ssl
|
||||
self.user = user
|
||||
self.password = password
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.access_key = None
|
||||
|
||||
self.ensure_connection()
|
||||
@@ -109,25 +104,49 @@ class ZadaraVPSAConnection(object):
|
||||
vpsa_commands = {
|
||||
'login': ('POST',
|
||||
'/api/users/login.xml',
|
||||
{'user': self.user,
|
||||
'password': self.password}),
|
||||
{'user': self.conf.zadara_user,
|
||||
'password': self.conf.zadara_password}),
|
||||
|
||||
# Volume operations
|
||||
'create_volume': ('POST',
|
||||
'/api/volumes.xml',
|
||||
{'display_name': kwargs.get('name'),
|
||||
'virtual_capacity': kwargs.get('size'),
|
||||
'raid_group_name[]': CONF.zadara_vpsa_poolname,
|
||||
'quantity': 1,
|
||||
'cache': CONF.zadara_default_cache_policy,
|
||||
'crypt': CONF.zadara_default_encryption,
|
||||
'mode': CONF.zadara_default_striping_mode,
|
||||
'stripesize': CONF.zadara_default_stripesize,
|
||||
'force': 'NO'}),
|
||||
{'name': kwargs.get('name'),
|
||||
'capacity': kwargs.get('size'),
|
||||
'pool': self.conf.zadara_vpsa_poolname,
|
||||
'thin': 'YES'
|
||||
if self.conf.zadara_vol_thin else 'NO',
|
||||
'crypt': 'YES'
|
||||
if self.conf.zadara_vol_encrypt else 'NO'}),
|
||||
'delete_volume': ('DELETE',
|
||||
'/api/volumes/%s.xml' % kwargs.get('vpsa_vol'),
|
||||
{}),
|
||||
|
||||
'expand_volume': ('POST',
|
||||
'/api/volumes/%s/expand.xml'
|
||||
% kwargs.get('vpsa_vol'),
|
||||
{'capacity': kwargs.get('size')}),
|
||||
|
||||
# Snapshot operations
|
||||
'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',
|
||||
@@ -150,6 +169,9 @@ class ZadaraVPSAConnection(object):
|
||||
'list_volumes': ('GET',
|
||||
'/api/volumes.xml',
|
||||
{}),
|
||||
'list_pools': ('GET',
|
||||
'/api/pools.xml',
|
||||
{}),
|
||||
'list_controllers': ('GET',
|
||||
'/api/vcontrollers.xml',
|
||||
{}),
|
||||
@@ -159,7 +181,11 @@ class ZadaraVPSAConnection(object):
|
||||
'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.keys():
|
||||
raise exception.UnknownCmd(cmd=cmd)
|
||||
@@ -218,10 +244,12 @@ class ZadaraVPSAConnection(object):
|
||||
LOG.debug(_('Sending %(method)s to %(url)s. Body "%(body)s"'),
|
||||
{'method': method, 'url': url, 'body': body})
|
||||
|
||||
if self.use_ssl:
|
||||
connection = httplib.HTTPSConnection(self.host, self.port)
|
||||
if self.conf.zadara_vpsa_use_ssl:
|
||||
connection = httplib.HTTPSConnection(self.conf.zadara_vpsa_ip,
|
||||
self.conf.zadara_vpsa_port)
|
||||
else:
|
||||
connection = httplib.HTTPConnection(self.host, self.port)
|
||||
connection = httplib.HTTPConnection(self.conf.zadara_vpsa_ip,
|
||||
self.conf.zadara_vpsa_port)
|
||||
connection.request(method, url, body)
|
||||
response = connection.getresponse()
|
||||
|
||||
@@ -244,19 +272,18 @@ class ZadaraVPSAConnection(object):
|
||||
class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
"""Zadara VPSA iSCSI volume driver."""
|
||||
|
||||
VERSION = '13.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(CONF.zadara_vpsa_ip,
|
||||
CONF.zadara_vpsa_port,
|
||||
CONF.zadara_vpsa_use_ssl,
|
||||
CONF.zadara_user,
|
||||
CONF.zadara_password)
|
||||
self.vpsa = ZadaraVPSAConnection(self.configuration)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Returns an error (exception) if prerequisites aren't met."""
|
||||
@@ -291,16 +318,57 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
result_list.append(object)
|
||||
return result_list if result_list else None
|
||||
|
||||
def _get_vpsa_volume_name(self, name):
|
||||
"""Return VPSA's name for the volume."""
|
||||
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')
|
||||
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."""
|
||||
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 ('infinite', 'infinite')
|
||||
|
||||
def _get_active_controller_details(self):
|
||||
"""Return details of VPSA's active controller."""
|
||||
xml_tree = self.vpsa.send_cmd('list_controllers')
|
||||
@@ -334,7 +402,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
"""Create volume."""
|
||||
self.vpsa.send_cmd(
|
||||
'create_volume',
|
||||
name=CONF.zadara_vol_name_template % volume['name'],
|
||||
name=self.configuration.zadara_vol_name_template % volume['name'],
|
||||
size=volume['size'])
|
||||
|
||||
def delete_volume(self, volume):
|
||||
@@ -344,13 +412,13 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
Return ok if doesn't exist. Auto detach from all servers.
|
||||
"""
|
||||
# Get volume name
|
||||
name = CONF.zadara_vol_name_template % volume['name']
|
||||
name = self.configuration.zadara_vol_name_template % volume['name']
|
||||
vpsa_vol = self._get_vpsa_volume_name(name)
|
||||
if not vpsa_vol:
|
||||
msg = _('Volume %(name)s could not be found. '
|
||||
'It might be already deleted') % {'name': name}
|
||||
LOG.warning(msg)
|
||||
if CONF.zadara_vpsa_allow_nonexistent_delete:
|
||||
if self.configuration.zadara_vpsa_allow_nonexistent_delete:
|
||||
return
|
||||
else:
|
||||
raise exception.VolumeNotFound(volume_id=name)
|
||||
@@ -361,7 +429,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
servers = self._xml_parse_helper(xml_tree, 'servers',
|
||||
('iqn', None), first=False)
|
||||
if servers:
|
||||
if not CONF.zadara_vpsa_auto_detach_on_delete:
|
||||
if not self.configuration.zadara_vpsa_auto_detach_on_delete:
|
||||
raise exception.VolumeAttached(volume_id=name)
|
||||
|
||||
for server in servers:
|
||||
@@ -374,6 +442,116 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
# 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.VolumeNotFound(volume_id=volume_name)
|
||||
|
||||
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(_("snapshot: original volume %s not found, "
|
||||
"skipping delete operation")
|
||||
% snapshot['volume_name'])
|
||||
return True
|
||||
|
||||
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(_("snapshot: snapshot %s not found, "
|
||||
"skipping delete operation")
|
||||
% snapshot['name'])
|
||||
return True
|
||||
|
||||
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:
|
||||
msg = _('Volume %(name)s not found') % {'name': volume_name}
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeNotFound(volume_id=volume_name)
|
||||
|
||||
snap_id = self._get_snap_id(cg_name, snapshot['name'])
|
||||
if not snap_id:
|
||||
msg = _('Snapshot %(name)s not found') % {'name': snapshot['name']}
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeNotFound(volume_id=snapshot['name'])
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
msg = _('Volume %(name)s not found') % {'name': volume_name}
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeNotFound(volume_id=volume_name)
|
||||
|
||||
self.vpsa.send_cmd('create_clone',
|
||||
cg_name=cg_name,
|
||||
name=self.configuration.zadara_vol_name_template
|
||||
% volume['name'])
|
||||
|
||||
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.VolumeNotFound(volume_id=name)
|
||||
|
||||
if new_size < size:
|
||||
raise exception.InvalidInput(
|
||||
reason='%s < current size %s' % (new_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):
|
||||
"""Irrelevant for VPSA volumes. Export created during attachment."""
|
||||
pass
|
||||
@@ -404,7 +582,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
raise exception.ZadaraServerCreateFailure(name=initiator_name)
|
||||
|
||||
# Get volume name
|
||||
name = CONF.zadara_vol_name_template % 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=name)
|
||||
@@ -460,7 +638,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
raise exception.ZadaraServerNotFound(name=initiator_name)
|
||||
|
||||
# Get volume name
|
||||
name = CONF.zadara_vol_name_template % 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=name)
|
||||
@@ -469,3 +647,33 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
|
||||
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')
|
||||
data["volume_backend_name"] = backend_name or self.__class__.__name__
|
||||
data["vendor_name"] = 'Zadara Storage'
|
||||
data["driver_version"] = self.VERSION
|
||||
data["storage_protocol"] = 'iSCSI'
|
||||
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
|
||||
|
||||
@@ -1647,11 +1647,11 @@
|
||||
# Name of VPSA storage pool for volumes (string value)
|
||||
#zadara_vpsa_poolname=<None>
|
||||
|
||||
# Default cache policy for volumes (string value)
|
||||
#zadara_default_cache_policy=write-through
|
||||
# Default thin provisioning policy for volumes (boolean value)
|
||||
#zadara_vol_thin=true
|
||||
|
||||
# Default encryption policy for volumes (string value)
|
||||
#zadara_default_encryption=NO
|
||||
# Default encryption policy for volumes (boolean value)
|
||||
#zadara_vol_encrypt=false
|
||||
|
||||
# Default striping mode for volumes (string value)
|
||||
#zadara_default_striping_mode=simple
|
||||
|
||||
Reference in New Issue
Block a user