diff --git a/cinder/tests/unit/test_nexenta5_iscsi.py b/cinder/tests/unit/test_nexenta5_iscsi.py new file mode 100644 index 00000000000..91074b8b6d9 --- /dev/null +++ b/cinder/tests/unit/test_nexenta5_iscsi.py @@ -0,0 +1,211 @@ +# Copyright 2016 Nexenta Systems, 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. +""" +Unit tests for OpenStack Cinder volume driver +""" + +import mock +from mock import patch +from oslo_utils import units + +from cinder import context +from cinder import db +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +from cinder.volume.drivers.nexenta.ns5 import iscsi +from cinder.volume.drivers.nexenta.ns5 import jsonrpc + + +class TestNexentaISCSIDriver(test.TestCase): + TEST_VOLUME_NAME = 'volume1' + TEST_VOLUME_NAME2 = 'volume2' + TEST_SNAPSHOT_NAME = 'snapshot1' + TEST_VOLUME_REF = { + 'name': TEST_VOLUME_NAME, + 'size': 1, + 'id': '1', + 'status': 'available' + } + TEST_VOLUME_REF2 = { + 'name': TEST_VOLUME_NAME2, + 'size': 1, + 'id': '2', + 'status': 'in-use' + } + TEST_SNAPSHOT_REF = { + 'name': TEST_SNAPSHOT_NAME, + 'volume_name': TEST_VOLUME_NAME, + 'volume_id': '1' + } + + def __init__(self, method): + super(TestNexentaISCSIDriver, self).__init__(method) + + def setUp(self): + super(TestNexentaISCSIDriver, self).setUp() + self.cfg = mock.Mock(spec=conf.Configuration) + self.ctxt = context.get_admin_context() + self.cfg.nexenta_dataset_description = '' + self.cfg.nexenta_host = '1.1.1.1' + self.cfg.nexenta_user = 'admin' + self.cfg.nexenta_password = 'nexenta' + self.cfg.nexenta_volume = 'cinder' + self.cfg.nexenta_rest_port = 2000 + self.cfg.nexenta_rest_protocol = 'http' + self.cfg.nexenta_iscsi_target_portal_port = 8080 + self.cfg.nexenta_target_prefix = 'iqn:' + self.cfg.nexenta_target_group_prefix = 'cinder/' + self.cfg.nexenta_ns5_blocksize = 32 + self.cfg.nexenta_sparse = True + self.cfg.nexenta_dataset_compression = 'on' + self.cfg.nexenta_dataset_dedup = 'off' + self.cfg.reserved_percentage = 20 + self.cfg.nexenta_volume = 'pool' + self.cfg.nexenta_volume_group = 'dsg' + self.nef_mock = mock.Mock() + self.stubs.Set(jsonrpc, 'NexentaJSONProxy', + lambda *_, **__: self.nef_mock) + self.drv = iscsi.NexentaISCSIDriver( + configuration=self.cfg) + self.drv.db = db + self.drv.do_setup(self.ctxt) + + def _create_volume_db_entry(self): + vol = { + 'id': '1', + 'size': 1, + 'status': 'available', + 'provider_location': self.TEST_VOLUME_NAME + } + return db.volume_create(self.ctxt, vol)['id'] + + def check_for_setup_error(self): + self.nef_mock.get.return_value = { + 'services': {'data': {'iscsit': {'state': 'offline'}}}} + self.assertRaises( + exception.NexentaException, self.drv.check_for_setup_error) + + def test_create_volume(self): + self.drv.create_volume(self.TEST_VOLUME_REF) + url = 'storage/pools/pool/volumeGroups/dsg/volumes' + self.nef_mock.post.assert_called_with(url, { + 'name': self.TEST_VOLUME_REF['name'], + 'volumeSize': 1 * units.Gi, + 'volumeBlockSize': 32768, + 'sparseVolume': self.cfg.nexenta_sparse}) + + def test_delete_volume(self): + self.drv.delete_volume(self.TEST_VOLUME_REF) + url = 'storage/pools/pool/volumeGroups' + data = {'name': 'dsg', 'volumeBlockSize': 32768} + self.nef_mock.post.assert_called_with(url, data) + + def test_create_cloned_volume(self): + self._create_volume_db_entry() + vol = self.TEST_VOLUME_REF2 + src_vref = self.TEST_VOLUME_REF + + self.drv.create_cloned_volume(vol, src_vref) + url = 'storage/pools/pool/volumeGroups/dsg/volumes/volume2/promote' + self.nef_mock.post.assert_called_with(url) + + def test_create_snapshot(self): + self._create_volume_db_entry() + self.drv.create_snapshot(self.TEST_SNAPSHOT_REF) + url = 'storage/pools/pool/volumeGroups/dsg/volumes/volume-1/snapshots' + self.nef_mock.post.assert_called_with( + url, {'name': 'snapshot1'}) + + def test_get_target_by_alias(self): + self.nef_mock.get.return_value = {'data': []} + self.assertIsNone(self.drv._get_target_by_alias('1.1.1.1-0')) + + self.nef_mock.get.return_value = {'data': [{'name': 'iqn-0'}]} + self.assertEqual( + {'name': 'iqn-0'}, self.drv._get_target_by_alias('1.1.1.1-0')) + + def test_target_group_exists(self): + self.nef_mock.get.return_value = {'data': []} + self.assertFalse( + self.drv._target_group_exists({'data': [{'name': 'iqn-0'}]})) + + self.nef_mock.get.return_value = {'data': [{'name': '1.1.1.1-0'}]} + self.assertTrue(self.drv._target_group_exists( + {'data': [{'name': 'iqn-0'}]})) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_target_by_alias') + def test_create_target(self, target): + target.return_value = {'name': 'iqn-0'} + self.assertEqual('iqn-0', self.drv._create_target(0)) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._create_target') + def test_get_target_name(self, target_name): + self._create_volume_db_entry() + target_name.return_value = 'iqn-0' + self.drv.targets['iqn-0'] = [] + self.assertEqual( + 'iqn-0', self.drv._get_target_name(self.TEST_VOLUME_REF)) + + volume = self.TEST_VOLUME_REF + volume['provider_location'] = '1.1.1.1:8080,1 iqn-0 0' + self.nef_mock.get.return_value = {'data': [{'alias': '1.1.1.1-0'}]} + self.assertEqual( + 'iqn-0', self.drv._get_target_name(self.TEST_VOLUME_REF)) + self.assertEqual('1.1.1.1-0', self.drv.targetgroups['iqn-0']) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_targetgroup_name') + def test_get_lun_id(self, targetgroup): + targetgroup.return_value = '1.1.1.1-0' + self.nef_mock.get.return_value = {'data': [{'guid': '0'}]} + self.assertEqual('0', self.drv._get_lun_id(self.TEST_VOLUME_REF)) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_lun_id') + def test_lu_exists(self, lun_id): + lun_id.return_value = '0' + self.assertTrue(self.drv._lu_exists(self.TEST_VOLUME_REF)) + lun_id.side_effect = LookupError + self.assertFalse(self.drv._lu_exists(self.TEST_VOLUME_REF)) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_lun_id') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_targetgroup_name') + def test_get_lun(self, targetgroup, lun_id): + lun_id.return_value = '0' + targetgroup.return_value = '1.1.1.1-0' + self.nef_mock.get.return_value = {'data': [{'lunNumber': 0}]} + self.assertEqual(0, self.drv._get_lun(self.TEST_VOLUME_REF)) + + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_target_name') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_targetgroup_name') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._lu_exists') + @patch('cinder.volume.drivers.nexenta.ns5.iscsi.' + 'NexentaISCSIDriver._get_lun') + def test_do_export(self, get_lun, lu_exists, targetgroup, target): + target.return_value = 'iqn-0' + targetgroup.return_value = '1.1.1.1-0' + lu_exists.return_value = False + get_lun.return_value = 0 + self.assertEqual( + {'provider_location': '1.1.1.1:8080,1 iqn-0 0'}, + self.drv._do_export({}, self.TEST_VOLUME_REF)) diff --git a/cinder/volume/drivers/nexenta/ns5/__init__.py b/cinder/volume/drivers/nexenta/ns5/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/nexenta/ns5/iscsi.py b/cinder/volume/drivers/nexenta/ns5/iscsi.py new file mode 100644 index 00000000000..2e82f0316e3 --- /dev/null +++ b/cinder/volume/drivers/nexenta/ns5/iscsi.py @@ -0,0 +1,525 @@ +# Copyright 2016 Nexenta Systems, 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. +""" +:mod:`nexenta.iscsi` -- Driver to store volumes on Nexenta Appliance +===================================================================== + +.. automodule:: nexenta.volume +""" + +from oslo_log import log as logging +from oslo_utils import units + +from cinder import context +from cinder import db +from cinder import exception +from cinder.i18n import _, _LI, _LE, _LW +from cinder.volume import driver +from cinder.volume.drivers.nexenta.ns5 import jsonrpc +from cinder.volume.drivers.nexenta import options +from cinder.volume.drivers.nexenta import utils + +VERSION = '1.0.0' +LOG = logging.getLogger(__name__) + + +class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921 + """Executes volume driver commands on Nexenta Appliance. + + Version history: + 1.0.0 - Initial driver version. + """ + + VERSION = VERSION + + def __init__(self, *args, **kwargs): + super(NexentaISCSIDriver, self).__init__(*args, **kwargs) + self.nef = None + self.targets = {} + self.targetgroups = {} + if self.configuration: + self.configuration.append_config_values( + options.NEXENTA_CONNECTION_OPTS) + self.configuration.append_config_values( + options.NEXENTA_ISCSI_OPTS) + self.configuration.append_config_values( + options.NEXENTA_DATASET_OPTS) + self.configuration.append_config_values( + options.NEXENTA_RRMGR_OPTS) + self.nef_protocol = self.configuration.nexenta_rest_protocol + self.nef_host = self.configuration.nexenta_host + self.nef_port = self.configuration.nexenta_rest_port + self.nef_user = self.configuration.nexenta_user + self.nef_password = self.configuration.nexenta_password + self.storage_pool = self.configuration.nexenta_volume + self.volume_group = self.configuration.nexenta_volume_group + self.dataset_compression = ( + self.configuration.nexenta_dataset_compression) + self.dataset_deduplication = self.configuration.nexenta_dataset_dedup + self.dataset_description = ( + self.configuration.nexenta_dataset_description) + self.iscsi_target_portal_port = ( + self.configuration.nexenta_iscsi_target_portal_port) + + @property + def backend_name(self): + backend_name = None + if self.configuration: + backend_name = self.configuration.safe_get('volume_backend_name') + if not backend_name: + backend_name = self.__class__.__name__ + return backend_name + + def do_setup(self, context): + if self.nef_protocol == 'auto': + protocol, auto = 'http', True + else: + protocol, auto = self.nef_protocol, False + self.nef = jsonrpc.NexentaJSONProxy( + protocol, self.nef_host, self.nef_port, self.nef_user, + self.nef_password, auto=auto) + url = 'storage/pools/%s/volumeGroups' % self.storage_pool + data = { + 'name': self.volume_group, + 'volumeBlockSize': ( + self.configuration.nexenta_ns5_blocksize * units.Ki) + } + try: + self.nef.post(url, data) + except exception.NexentaException as e: + if 'EEXIST' in e.args[0]: + LOG.debug('volumeGroup already exists, skipping') + else: + raise + + def check_for_setup_error(self): + """Verify that the zfs volumes exist. + + :raise: :py:exc:`LookupError` + """ + url = 'storage/pools/%(pool)s/volumeGroups/%(group)s' % { + 'pool': self.storage_pool, + 'group': self.volume_group, + } + try: + self.nef.get(url) + except exception.NexentaException: + raise LookupError(_( + "Dataset group %s not found at Nexenta SA"), '/'.join( + [self.storage_pool, self.volume_group])) + services = self.nef.get('services') + for service in services['data']: + if service['name'] == 'iscsit': + if service['state'] != 'online': + raise exception.NexentaException( + 'iSCSI service is not running on NS appliance') + break + + def _get_volume_path(self, volume): + """Return zfs volume name that corresponds given volume name.""" + return '%s/%s/%s' % (self.storage_pool, self.volume_group, + volume['name']) + + def _create_target(self, target_idx): + target_alias = '%s-%i' % ( + self.nef_host, + target_idx + ) + + target = self._get_target_by_alias(target_alias) + if not target: + url = 'san/iscsi/targets' + data = {'alias': target_alias} + self.nef.post(url, data) + target = self._get_target_by_alias(target_alias) + if not self._target_group_exists(target_alias): + url = 'san/targetgroups' + data = {'name': target_alias, 'targets': [target['name']]} + self.nef.post(url, data) + + self.targetgroups[target['name']] = target_alias + self.targets[target['name']] = [] + return target['name'] + + def _get_target_name(self, volume): + """Return iSCSI target name with least LUs.""" + provider_location = volume.get('provider_location') + target_names = list(self.targets) + if provider_location: + target_name = provider_location.split(',1 ')[1].split(' ')[0] + if not self.targets.get(target_name): + self.targets[target_name] = [] + if not(volume['name'] in self.targets[target_name]): + self.targets[target_name].append(volume['name']) + if not self.targetgroups.get(target_name): + url = 'san/iscsi/targets' + data = self.nef.get(url).get('data') + target_alias = data[0]['alias'] + self.targetgroups[target_name] = target_alias + elif not target_names: + # create first target and target group + target_name = self._create_target(0) + self.targets[target_name].append(volume['name']) + else: + target_name = target_names[0] + for target in target_names: + # find target with minimum number of volumes + if len(self.targets[target]) < len(self.targets[target_name]): + target_name = target + if len(self.targets[target_name]) >= 20: + # create new target and target group + target_name = self._create_target(len(target_names)) + if not(volume['name'] in self.targets[target_name]): + self.targets[target_name].append(volume['name']) + return target_name + + def _get_targetgroup_name(self, volume): + target_name = self._get_target_name(volume) + return self.targetgroups[target_name] + + @staticmethod + def _get_clone_snapshot_name(volume): + """Return name for snapshot that will be used to clone the volume.""" + return 'cinder-clone-snapshot-%(id)s' % volume + + def create_volume(self, volume): + """Create a zfs volume on appliance. + + :param volume: volume reference + :return: model update dict for volume reference + """ + url = 'storage/pools/%(pool)s/volumeGroups/%(group)s/volumes' % { + 'pool': self.storage_pool, + 'group': self.volume_group, + } + data = { + 'name': volume['name'], + 'volumeSize': volume['size'] * units.Gi, + 'volumeBlockSize': ( + self.configuration.nexenta_ns5_blocksize * units.Ki), + 'sparseVolume': self.configuration.nexenta_sparse + } + self.nef.post(url, data) + + def delete_volume(self, volume): + """Destroy a zfs volume on appliance. + + :param volume: volume reference + """ + pool, group, name = self._get_volume_path(volume).split('/') + url = ('storage/pools/%(pool)s/volumeGroups/%(group)s' + '/volumes/%(name)s') % { + 'pool': pool, + 'group': group, + 'name': name + } + try: + self.nef.delete(url) + except exception.NexentaException as exc: + # We assume that volume is gone + LOG.warning(_LW('Got error trying to delete volume %(volume)s,' + ' assuming it is already gone: %(exc)s'), + {'volume': volume, 'exc': exc}) + + def extend_volume(self, volume, new_size): + """Extend an existing volume. + + :param volume: volume reference + :param new_size: volume new size in GB + """ + LOG.info(_LI('Extending volume: %(id)s New size: %(size)s GB'), + {'id': volume['id'], 'size': new_size}) + pool, group, name = self._get_volume_path(volume).split('/') + url = ('storage/pools/%(pool)s/volumeGroups/%(group)s/' + 'volumes/%(name)s') % { + 'pool': pool, + 'group': group, + 'name': name + } + self.nef.put(url, {'volumeSize': new_size * units.Gi}) + + def create_snapshot(self, snapshot): + """Creates a snapshot. + + :param snapshot: snapshot reference + """ + snapshot_vol = self._get_snapshot_volume(snapshot) + LOG.info(_LI('Creating snapshot %(snap)s of volume %(vol)s'), { + 'snap': snapshot['name'], + 'vol': snapshot_vol['name'] + }) + volume_path = self._get_volume_path(snapshot_vol) + pool, group, volume = volume_path.split('/') + url = ('storage/pools/%(pool)s/volumeGroups/%(group)s/' + 'volumes/%(volume)s/snapshots') % { + 'pool': pool, + 'group': group, + 'volume': snapshot_vol['name'] + } + self.nef.post(url, {'name': snapshot['name']}) + + def delete_snapshot(self, snapshot): + """Delete volume's snapshot on appliance. + + :param snapshot: snapshot reference + """ + LOG.info(_LI('Deleting snapshot: %s'), snapshot['name']) + snapshot_vol = self._get_snapshot_volume(snapshot) + volume_path = self._get_volume_path(snapshot_vol) + pool, group, volume = volume_path.split('/') + url = ('storage/pools/%(pool)s/volumeGroups/%(group)s/' + 'volumes/%(volume)s/snapshots/%(snapshot)s') % { + 'pool': pool, + 'group': group, + 'volume': volume, + 'snapshot': snapshot['name'] + } + try: + self.nef.delete(url) + except exception.NexentaException as exc: + if 'EBUSY' is exc: + LOG.warning(_LW( + 'Could not delete snapshot %s - it has dependencies'), + snapshot['name']) + else: + LOG.warning(exc) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create new volume from other's snapshot on appliance. + + :param volume: reference of volume to be created + :param snapshot: reference of source snapshot + """ + LOG.info(_LI('Creating volume from snapshot: %s'), snapshot['name']) + snapshot_vol = self._get_snapshot_volume(snapshot) + volume_path = self._get_volume_path(snapshot_vol) + pool, group, snapshot_vol = volume_path.split('/') + url = ('storage/pools/%(pool)s/volumeGroups/%(group)s/' + 'volumes/%(volume)s/snapshots/%(snapshot)s/clone') % { + 'pool': pool, + 'group': group, + 'volume': snapshot_vol, + 'snapshot': snapshot['name'] + } + targetPath = self._get_volume_path(volume) + self.nef.post(url, {'targetPath': targetPath}) + url = ('storage/pools/%(pool)s/volumeGroups/' + '%(group)s/volumes/%(name)s/promote') % { + 'pool': pool, + 'group': group, + 'name': volume['name'], + } + self.nef.post(url) + + def create_cloned_volume(self, volume, src_vref): + """Creates a clone of the specified volume. + + :param volume: new volume reference + :param src_vref: source volume reference + """ + snapshot = {'volume_name': src_vref['name'], + 'volume_id': src_vref['id'], + 'name': self._get_clone_snapshot_name(volume)} + LOG.debug('Creating temp snapshot of the original volume: ' + '%s@%s', snapshot['volume_name'], snapshot['name']) + # We don't delete this snapshot, because this snapshot will be origin + # of new volume. This snapshot will be automatically promoted by NEF + # when user will delete origin volume. But when cloned volume deleted + # we check its origin property and delete source snapshot if needed. + self.create_snapshot(snapshot) + try: + self.create_volume_from_snapshot(volume, snapshot) + except exception.NexentaException: + LOG.error(_LE('Volume creation failed, deleting created snapshot ' + '%s'), '@'.join( + [snapshot['volume_name'], snapshot['name']])) + try: + self.delete_snapshot(snapshot) + except (exception.NexentaException, exception.SnapshotIsBusy): + LOG.warning(_LW('Failed to delete zfs snapshot ' + '%s'), '@'.join( + [snapshot['volume_name'], snapshot['name']])) + raise + + def _get_snapshot_volume(self, snapshot): + ctxt = context.get_admin_context() + return db.volume_get(ctxt, snapshot['volume_id']) + + def _get_target_by_alias(self, alias): + """Get an iSCSI target by it's alias. + + :param alias: target alias + :return: First found target, else None + """ + url = 'san/iscsi/targets?alias=%s' % alias + targets = self.nef.get(url).get('data') + if not targets: + return None + return targets[0] + + def _target_group_exists(self, target_group): + """Check if target group exist. + + :param target_group: target group + :return: True if target group exist, else False + """ + url = 'san/targetgroups?name=%s' % target_group + return bool(self.nef.get(url).get('data')) + + def _lu_exists(self, volume): + """Check if LU exists on appliance. + + :param volume: cinder volume + :return: True if LU exists, else False + """ + try: + self._get_lun_id(volume) + except LookupError: + return False + return True + + def _get_lun_id(self, volume): + """Get lun id for zfs volume. + + :param volume: cinder volume + :raises: LookupError if zfs volume does not exist or not mapped to LU + :return: LUN + """ + volume_path = self._get_volume_path(volume) + targetgroup_name = self._get_targetgroup_name(volume) + url = 'san/targetgroups/%s/luns?volume=%s' % ( + targetgroup_name, volume_path.replace('/', '%2F')) + data = self.nef.get(url).get('data') + if not data: + raise LookupError(_("LU does not exist for volume: %s"), + volume['name']) + else: + return data[0]['guid'] + + def _get_lun(self, volume): + try: + lun_id = self._get_lun_id(volume) + except LookupError: + return None + targetgroup_name = self._get_targetgroup_name(volume) + url = 'san/targetgroups/%s/luns/%s/views' % ( + targetgroup_name, lun_id) + data = self.nef.get(url).get('data') + if not data: + raise LookupError(_("No views found for LUN: %s"), lun_id) + return data[0]['lunNumber'] + + def _do_export(self, _ctx, volume): + """Do all steps to get zfs volume exported at separate target. + + :param volume: reference of volume to be exported + """ + volume_path = self._get_volume_path(volume) + target_name = self._get_target_name(volume) + targetgroup_name = self._get_targetgroup_name(volume) + entry = {} + + if not self._lu_exists(volume): + url = 'san/targetgroups/%s/luns' % targetgroup_name + data = {'volume': volume_path} + self.nef.post(url, data) + entry['lun'] = self._get_lun(volume) + + model_update = {} + if entry.get('lun') is not None: + provider_location = '%(host)s:%(port)s,1 %(name)s %(lun)s' % { + 'host': self.nef_host, + 'port': self.configuration.nexenta_iscsi_target_portal_port, + 'name': target_name, + 'lun': entry['lun'], + } + model_update = {'provider_location': provider_location} + return model_update + + def create_export(self, _ctx, volume, connector): + """Create new export for zfs volume. + + :param volume: reference of volume to be exported + :return: iscsiadm-formatted provider location string + """ + model_update = self._do_export(_ctx, volume) + return model_update + + def ensure_export(self, _ctx, volume): + """Recreate parts of export if necessary. + + :param volume: reference of volume to be exported + """ + self._do_export(_ctx, volume) + + def remove_export(self, _ctx, volume): + """Destroy all resources created to export zfs volume. + + :param volume: reference of volume to be unexported + """ + try: + lun_id = self._get_lun_id(volume) + except LookupError: + return + targetgroup_name = self._get_targetgroup_name(volume) + url = 'san/targetgroups/%s/luns/%s' % ( + targetgroup_name, lun_id) + self.nef.delete(url) + + 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 for NexentaStor appliance.""" + LOG.debug('Updating volume stats') + + url = 'storage/pools/%(pool)s/volumeGroups/%(group)s' % { + 'pool': self.storage_pool, + 'group': self.volume_group, + } + stats = self.nef.get(url) + total_amount = utils.str2gib_size( + stats['bytesAvailable'] + stats['bytesUsed']) + free_amount = utils.str2gib_size(stats['bytesAvailable']) + + location_info = '%(driver)s:%(host)s:%(pool)s/%(group)s' % { + 'driver': self.__class__.__name__, + 'host': self.nef_host, + 'pool': self.storage_pool, + 'group': self.volume_group, + } + self._stats = { + 'vendor_name': 'Nexenta', + 'dedup': self.dataset_deduplication, + 'compression': self.dataset_compression, + 'description': self.dataset_description, + 'driver_version': self.VERSION, + 'storage_protocol': 'iSCSI', + 'total_capacity_gb': total_amount, + 'free_capacity_gb': free_amount, + 'reserved_percentage': self.configuration.reserved_percentage, + 'QoS_support': False, + 'volume_backend_name': self.backend_name, + 'location_info': location_info, + 'iscsi_target_portal_port': self.iscsi_target_portal_port, + 'nef_url': self.nef.url + } diff --git a/cinder/volume/drivers/nexenta/ns5/jsonrpc.py b/cinder/volume/drivers/nexenta/ns5/jsonrpc.py new file mode 100644 index 00000000000..c37c49cdd3a --- /dev/null +++ b/cinder/volume/drivers/nexenta/ns5/jsonrpc.py @@ -0,0 +1,105 @@ +# Copyright 2011 Nexenta Systems, 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. +""" +:mod:`nexenta.jsonrpc` -- Nexenta-specific JSON RPC client +===================================================================== + +.. automodule:: nexenta.jsonrpc +""" + +import time + +from oslo_log import log as logging +from oslo_serialization import jsonutils +import requests + +from cinder import exception + +LOG = logging.getLogger(__name__) + + +class NexentaJSONProxy(object): + + def __init__(self, scheme, host, port, user, + password, auto=False, method=None): + self.scheme = scheme + self.host = host + self.port = port + self.user = user + self.password = password + self.auto = True + self.method = method + + @property + def url(self): + return '%s://%s:%s/' % (self.scheme, self.host, self.port) + + def __getattr__(self, method=None): + if method: + return NexentaJSONProxy( + self.scheme, self.host, self.port, + self.user, self.password, self.auto, method) + + def __hash__(self): + return self.url.__hash__() + + def __repr__(self): + return 'NEF proxy: %s' % self.url + + def __call__(self, path, data=None): + auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1] + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Basic %s' % auth + } + url = self.url + path + + if data: + data = jsonutils.dumps(data) + + LOG.debug('Sending JSON to url: %s, data: %s, method: %s', + path, data, self.method) + if self.method == 'get': + resp = requests.get(url, headers=headers) + if self.method == 'post': + resp = requests.post(url, data=data, headers=headers) + if self.method == 'put': + resp = requests.put(url, data=data, headers=headers) + if self.method == 'delete': + resp = requests.delete(url, data=data, headers=headers) + + if resp.status_code == 201 or ( + resp.status_code == 200 and not resp.content): + LOG.debug('Got response: Success') + return 'Success' + + response = resp.json() + resp.close() + if response and resp.status_code == 202: + url = self.url + response['links'][0]['href'] + while resp.status_code == 202: + time.sleep(1) + resp = requests.get(url) + if resp.status_code == 201 or ( + resp.status_code == 200 and not resp.content): + LOG.debug('Got response: Success') + return 'Success' + else: + response = resp.json() + resp.close() + if response.get('code'): + raise exception.NexentaException(response) + LOG.debug('Got response: %s', response) + return response diff --git a/cinder/volume/drivers/nexenta/options.py b/cinder/volume/drivers/nexenta/options.py index 68eeedd4c3e..eed18a00d96 100644 --- a/cinder/volume/drivers/nexenta/options.py +++ b/cinder/volume/drivers/nexenta/options.py @@ -81,6 +81,9 @@ NEXENTA_ISCSI_OPTS = [ cfg.StrOpt('nexenta_target_group_prefix', default='cinder/', help='Prefix for iSCSI target groups on SA'), + cfg.StrOpt('nexenta_volume_group', + default='iscsi', + help='Volume group for ns5'), ] NEXENTA_NFS_OPTS = [ diff --git a/releasenotes/notes/nexentastor5_iscsi-e1d88b07d15c660b.yaml b/releasenotes/notes/nexentastor5_iscsi-e1d88b07d15c660b.yaml new file mode 100644 index 00000000000..677d864102c --- /dev/null +++ b/releasenotes/notes/nexentastor5_iscsi-e1d88b07d15c660b.yaml @@ -0,0 +1,2 @@ +features: + - Added backend driver for NexentaStor5 iSCSI storage.