diff --git a/cinder/tests/unit/test_nexenta_edge.py b/cinder/tests/unit/test_nexenta_edge.py index 83a142322ef..129624e9e76 100644 --- a/cinder/tests/unit/test_nexenta_edge.py +++ b/cinder/tests/unit/test_nexenta_edge.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import mock from cinder import context @@ -56,8 +57,14 @@ ISCSI_TARGET_STATUS = 'Target 1: ' + ISCSI_TARGET_NAME class TestNexentaEdgeISCSIDriver(test.TestCase): def setUp(self): + def _safe_get(opt): + return getattr(self.cfg, opt) super(TestNexentaEdgeISCSIDriver, self).setUp() + self.context = context.get_admin_context() 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_rest_address = '0.0.0.0' self.cfg.nexenta_rest_port = 8080 @@ -82,13 +89,82 @@ class TestNexentaEdgeISCSIDriver(test.TestCase): self.mock_api.return_value = { '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) def test_check_do_setup(self): 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): self.driver.create_volume(MOCK_VOL) self.mock_api.assert_called_with(NEDGE_URL, { @@ -98,6 +174,17 @@ class TestNexentaEdgeISCSIDriver(test.TestCase): '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): self.mock_api.side_effect = RuntimeError self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL) diff --git a/cinder/volume/drivers/nexenta/nexentaedge/iscsi.py b/cinder/volume/drivers/nexenta/nexentaedge/iscsi.py index 8d20fdef33f..76a52270a89 100644 --- a/cinder/volume/drivers/nexenta/nexentaedge/iscsi.py +++ b/cinder/volume/drivers/nexenta/nexentaedge/iscsi.py @@ -25,6 +25,7 @@ from cinder import interface from cinder.volume import driver from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc from cinder.volume.drivers.nexenta import options +from cinder.volume.drivers.nexenta import utils as nexenta_utils LOG = logging.getLogger(__name__) @@ -37,9 +38,10 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): Version history: 1.0.0 - Initial driver version. 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): super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs) @@ -67,6 +69,7 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): self.iscsi_target_port = (self.configuration. nexenta_iscsi_target_portal_port) self.target_vip = None + self.ha_vip = None @property def backend_name(self): @@ -78,6 +81,13 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): return backend_name 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': protocol, auto = 'http', True else: @@ -93,22 +103,37 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): data_keys = rsp['data'][list(rsp['data'].keys())[0]] 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) if 'X-VIPS' in rsp['data']: vips = json.loads(rsp['data']['X-VIPS']) - if len(vips[0]) == 1: - self.target_vip = vips[0][0]['ip'].split('/', 1)[0] + vips = [get_ip(host) for host in vips] + 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: - self.target_vip = vips[0][1]['ip'].split('/', 1)[0] - else: - self.target_vip = self.configuration.safe_get( - 'nexenta_client_address') - if not self.target_vip: - LOG.error(_LE('No VIP configured for service %s'), - self.iscsi_service) - raise exception.VolumeBackendAPIException( - _('No service VIP configured and ' - 'no nexenta_client_address')) + if len(vips) == 1: + target_vip = vips[0]['ip'] + self.ha_vip = '/'.join( + (vips[0]['ip'], vips[0]['mask'])) + if not target_vip: + LOG.error(_LE('No VIP configured for service %s'), + self.iscsi_service) + raise exception.VolumeBackendAPIException( + message=_('No service VIP configured and ' + 'no nexenta_client_address')) + self.target_vip = target_vip except exception.VolumeBackendAPIException: with excutils.save_and_reraise_exception(): LOG.exception(_LE('Error verifying iSCSI service %(serv)s on ' @@ -149,13 +174,16 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): } def create_volume(self, volume): + data = { + 'objectPath': self.bucket_path + '/' + volume['name'], + 'volSizeMB': int(volume['size']) * units.Ki, + 'blockSize': self.blocksize, + 'chunkSize': self.chunksize + } + if self.ha_vip: + data['vip'] = self.ha_vip try: - self.restapi.post('service/' + self.iscsi_service + '/iscsi', { - 'objectPath': self.bucket_path + '/' + volume['name'], - 'volSizeMB': int(volume['size']) * units.Ki, - 'blockSize': self.blocksize, - 'chunkSize': self.chunksize - }) + self.restapi.post('service/' + self.iscsi_service + '/iscsi', data) except exception.VolumeBackendAPIException: with excutils.save_and_reraise_exception(): LOG.exception(_LE('Error creating volume')) @@ -239,8 +267,7 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): except exception.VolumeBackendAPIException: with excutils.save_and_reraise_exception(): LOG.exception(_LE('Error creating cloned volume')) - - if (('size' in volume) and (volume['size'] > src_vref['size'])): + if volume['size'] > src_vref['size']: self.extend_volume(volume, volume['size']) def create_export(self, context, volume, connector=None): @@ -256,6 +283,11 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): raise NotImplementedError 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' % { 'driver': self.__class__.__name__, 'host': self._get_target_address(None), @@ -266,8 +298,8 @@ class NexentaEdgeISCSIDriver(driver.ISCSIDriver): 'driver_version': self.VERSION, 'storage_protocol': 'iSCSI', 'reserved_percentage': 0, - 'total_capacity_gb': 'unknown', - 'free_capacity_gb': 'unknown', + 'total_capacity_gb': total, + 'free_capacity_gb': free, 'QoS_support': False, 'volume_backend_name': self.backend_name, 'location_info': location_info, diff --git a/releasenotes/notes/nexentaedge-iscsi-ee5d6c05d65f97af.yaml b/releasenotes/notes/nexentaedge-iscsi-ee5d6c05d65f97af.yaml new file mode 100644 index 00000000000..ceeea9881db --- /dev/null +++ b/releasenotes/notes/nexentaedge-iscsi-ee5d6c05d65f97af.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added HA support for NexentaEdge iSCSI driver