Nexenta: Added HA support and enhance get_volume_stats()

Added HA support in NexentaEdge iSCSI driver

get_volume_stats function now returns total and free capacity

Change-Id: I7a27d94fef9309ebd1bb82dbb2c518f917ab32ca
This commit is contained in:
Aleksey Ruban 2016-06-29 07:35:20 -06:00
parent 483bc008f3
commit 672120b372
3 changed files with 146 additions and 24 deletions

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
import mock import mock
from cinder import context from cinder import context
@ -56,8 +57,14 @@ ISCSI_TARGET_STATUS = 'Target 1: ' + ISCSI_TARGET_NAME
class TestNexentaEdgeISCSIDriver(test.TestCase): class TestNexentaEdgeISCSIDriver(test.TestCase):
def setUp(self): def setUp(self):
def _safe_get(opt):
return getattr(self.cfg, opt)
super(TestNexentaEdgeISCSIDriver, self).setUp() super(TestNexentaEdgeISCSIDriver, self).setUp()
self.context = context.get_admin_context()
self.cfg = mock.Mock(spec=conf.Configuration) self.cfg = mock.Mock(spec=conf.Configuration)
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
self.cfg.trace_flags = 'fake_trace_flags'
self.cfg.driver_data_namespace = 'fake_driver_data_namespace'
self.cfg.nexenta_client_address = '0.0.0.0' self.cfg.nexenta_client_address = '0.0.0.0'
self.cfg.nexenta_rest_address = '0.0.0.0' self.cfg.nexenta_rest_address = '0.0.0.0'
self.cfg.nexenta_rest_port = 8080 self.cfg.nexenta_rest_port = 8080
@ -82,13 +89,82 @@ class TestNexentaEdgeISCSIDriver(test.TestCase):
self.mock_api.return_value = { self.mock_api.return_value = {
'data': {'value': ISCSI_TARGET_STATUS} 'data': {'value': ISCSI_TARGET_STATUS}
} }
self.driver.do_setup(context.get_admin_context()) self.driver.do_setup(self.context)
self.addCleanup(self.api_patcher.stop) self.addCleanup(self.api_patcher.stop)
def test_check_do_setup(self): def test_check_do_setup(self):
self.assertEqual(ISCSI_TARGET_NAME, self.driver.target_name) self.assertEqual(ISCSI_TARGET_NAME, self.driver.target_name)
def test_check_do_setup__vip(self):
first_vip = '/'.join((self.cfg.nexenta_client_address, '32'))
vips = [
[{'ip': first_vip}],
[{'ip': '0.0.0.1/32'}]
]
def my_side_effect(*args, **kwargs):
if args[0] == 'service/isc/iscsi/status':
return {'data': {'value': ISCSI_TARGET_STATUS}}
else:
return {'data': {'X-VIPS': json.dumps(vips)}}
self.mock_api.side_effect = my_side_effect
self.driver.do_setup(self.context)
self.assertEqual(self.driver.ha_vip, first_vip)
def test_check_do_setup__vip_not_in_xvips(self):
first_vip = '1.2.3.4/32'
vips = [
[{'ip': first_vip}],
[{'ip': '0.0.0.1/32'}]
]
def my_side_effect(*args, **kwargs):
if args[0] == 'service/isc/iscsi/status':
return {'data': {'value': ISCSI_TARGET_STATUS}}
else:
return {'data': {'X-VIPS': json.dumps(vips)}}
self.mock_api.side_effect = my_side_effect
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.do_setup, self.context)
def test_check_do_setup__vip_no_client_address(self):
self.cfg.nexenta_client_address = None
first_vip = '1.2.3.4/32'
vips = [
[{'ip': first_vip}]
]
def my_side_effect(*args, **kwargs):
if args[0] == 'service/isc/iscsi/status':
return {'data': {'value': ISCSI_TARGET_STATUS}}
else:
return {'data': {'X-VIPS': json.dumps(vips)}}
self.mock_api.side_effect = my_side_effect
self.driver.do_setup(self.context)
self.assertEqual(self.driver.ha_vip, first_vip)
def test_check_do_setup__vip_no_client_address_2_xvips(self):
self.cfg.nexenta_client_address = None
first_vip = '1.2.3.4/32'
vips = [
[{'ip': first_vip}],
[{'ip': '0.0.0.1/32'}]
]
def my_side_effect(*args, **kwargs):
if args[0] == 'service/isc/iscsi/status':
return {'data': {'value': ISCSI_TARGET_STATUS}}
else:
return {'data': {'X-VIPS': json.dumps(vips)}}
self.mock_api.side_effect = my_side_effect
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.do_setup, self.context)
def test_create_volume(self): def test_create_volume(self):
self.driver.create_volume(MOCK_VOL) self.driver.create_volume(MOCK_VOL)
self.mock_api.assert_called_with(NEDGE_URL, { self.mock_api.assert_called_with(NEDGE_URL, {
@ -98,6 +174,17 @@ class TestNexentaEdgeISCSIDriver(test.TestCase):
'chunkSize': NEDGE_CHUNKSIZE 'chunkSize': NEDGE_CHUNKSIZE
}) })
def test_create_volume__vip(self):
self.driver.ha_vip = self.cfg.nexenta_client_address + '/32'
self.driver.create_volume(MOCK_VOL)
self.mock_api.assert_called_with(NEDGE_URL, {
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
'volSizeMB': MOCK_VOL['size'] * 1024,
'blockSize': NEDGE_BLOCKSIZE,
'chunkSize': NEDGE_CHUNKSIZE,
'vip': self.cfg.nexenta_client_address + '/32'
})
def test_create_volume_fail(self): def test_create_volume_fail(self):
self.mock_api.side_effect = RuntimeError self.mock_api.side_effect = RuntimeError
self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL) self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL)

View File

@ -25,6 +25,7 @@ from cinder import interface
from cinder.volume import driver from cinder.volume import driver
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
from cinder.volume.drivers.nexenta import options from cinder.volume.drivers.nexenta import options
from cinder.volume.drivers.nexenta import utils as nexenta_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -37,9 +38,10 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
Version history: Version history:
1.0.0 - Initial driver version. 1.0.0 - Initial driver version.
1.0.1 - Moved opts to options.py. 1.0.1 - Moved opts to options.py.
1.0.2 - Added HA support.
""" """
VERSION = '1.0.1' VERSION = '1.0.2'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs) super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs)
@ -67,6 +69,7 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
self.iscsi_target_port = (self.configuration. self.iscsi_target_port = (self.configuration.
nexenta_iscsi_target_portal_port) nexenta_iscsi_target_portal_port)
self.target_vip = None self.target_vip = None
self.ha_vip = None
@property @property
def backend_name(self): def backend_name(self):
@ -78,6 +81,13 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
return backend_name return backend_name
def do_setup(self, context): def do_setup(self, context):
def get_ip(host):
hm = host[0 if len(host) == 1 else 1]['ip'].split('/', 1)
return {
'ip': hm[0],
'mask': hm[1] if len(hm) > 1 else '32'
}
if self.restapi_protocol == 'auto': if self.restapi_protocol == 'auto':
protocol, auto = 'http', True protocol, auto = 'http', True
else: else:
@ -93,22 +103,37 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
data_keys = rsp['data'][list(rsp['data'].keys())[0]] data_keys = rsp['data'][list(rsp['data'].keys())[0]]
self.target_name = data_keys.split('\n', 1)[0].split(' ')[2] self.target_name = data_keys.split('\n', 1)[0].split(' ')[2]
target_vip = self.configuration.safe_get(
'nexenta_client_address')
rsp = self.restapi.get('service/' + self.iscsi_service) rsp = self.restapi.get('service/' + self.iscsi_service)
if 'X-VIPS' in rsp['data']: if 'X-VIPS' in rsp['data']:
vips = json.loads(rsp['data']['X-VIPS']) vips = json.loads(rsp['data']['X-VIPS'])
if len(vips[0]) == 1: vips = [get_ip(host) for host in vips]
self.target_vip = vips[0][0]['ip'].split('/', 1)[0] if target_vip:
found = False
for host in vips:
if target_vip == host['ip']:
self.ha_vip = '/'.join((host['ip'], host['mask']))
found = True
break
if not found:
raise exception.VolumeBackendAPIException(
message=_("nexenta_client_address doesn't match "
"any VIPs provided by service: {}"
).format(
", ".join([host['ip'] for host in vips])))
else: else:
self.target_vip = vips[0][1]['ip'].split('/', 1)[0] if len(vips) == 1:
else: target_vip = vips[0]['ip']
self.target_vip = self.configuration.safe_get( self.ha_vip = '/'.join(
'nexenta_client_address') (vips[0]['ip'], vips[0]['mask']))
if not self.target_vip: if not target_vip:
LOG.error(_LE('No VIP configured for service %s'), LOG.error(_LE('No VIP configured for service %s'),
self.iscsi_service) self.iscsi_service)
raise exception.VolumeBackendAPIException( raise exception.VolumeBackendAPIException(
_('No service VIP configured and ' message=_('No service VIP configured and '
'no nexenta_client_address')) 'no nexenta_client_address'))
self.target_vip = target_vip
except exception.VolumeBackendAPIException: except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error verifying iSCSI service %(serv)s on ' LOG.exception(_LE('Error verifying iSCSI service %(serv)s on '
@ -149,13 +174,16 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
} }
def create_volume(self, volume): def create_volume(self, volume):
try: data = {
self.restapi.post('service/' + self.iscsi_service + '/iscsi', {
'objectPath': self.bucket_path + '/' + volume['name'], 'objectPath': self.bucket_path + '/' + volume['name'],
'volSizeMB': int(volume['size']) * units.Ki, 'volSizeMB': int(volume['size']) * units.Ki,
'blockSize': self.blocksize, 'blockSize': self.blocksize,
'chunkSize': self.chunksize 'chunkSize': self.chunksize
}) }
if self.ha_vip:
data['vip'] = self.ha_vip
try:
self.restapi.post('service/' + self.iscsi_service + '/iscsi', data)
except exception.VolumeBackendAPIException: except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error creating volume')) LOG.exception(_LE('Error creating volume'))
@ -239,8 +267,7 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
except exception.VolumeBackendAPIException: except exception.VolumeBackendAPIException:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error creating cloned volume')) LOG.exception(_LE('Error creating cloned volume'))
if volume['size'] > src_vref['size']:
if (('size' in volume) and (volume['size'] > src_vref['size'])):
self.extend_volume(volume, volume['size']) self.extend_volume(volume, volume['size'])
def create_export(self, context, volume, connector=None): def create_export(self, context, volume, connector=None):
@ -256,6 +283,11 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
raise NotImplementedError raise NotImplementedError
def get_volume_stats(self, refresh=False): def get_volume_stats(self, refresh=False):
resp = self.restapi.get('system/stats')
summary = resp['stats']['summary']
total = nexenta_utils.str2gib_size(summary['total_capacity'])
free = nexenta_utils.str2gib_size(summary['total_available'])
location_info = '%(driver)s:%(host)s:%(bucket)s' % { location_info = '%(driver)s:%(host)s:%(bucket)s' % {
'driver': self.__class__.__name__, 'driver': self.__class__.__name__,
'host': self._get_target_address(None), 'host': self._get_target_address(None),
@ -266,8 +298,8 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
'driver_version': self.VERSION, 'driver_version': self.VERSION,
'storage_protocol': 'iSCSI', 'storage_protocol': 'iSCSI',
'reserved_percentage': 0, 'reserved_percentage': 0,
'total_capacity_gb': 'unknown', 'total_capacity_gb': total,
'free_capacity_gb': 'unknown', 'free_capacity_gb': free,
'QoS_support': False, 'QoS_support': False,
'volume_backend_name': self.backend_name, 'volume_backend_name': self.backend_name,
'location_info': location_info, 'location_info': location_info,

View File

@ -0,0 +1,3 @@
---
features:
- Added HA support for NexentaEdge iSCSI driver