diff options
| author | Yuriy Taraday <yorik.sar@gmail.com> | 2012-02-15 21:31:51 +0400 |
|---|---|---|
| committer | Yuriy Taraday <yorik.sar@gmail.com> | 2012-02-23 00:43:27 +0400 |
| commit | 4ba4fb6a4e09e4be9a17f6da78820834a6676b92 (patch) | |
| tree | aaa705c38334c3b4fe82920949cf31be129c3da7 | |
| parent | 160e6b6eee05f4273dc52770575d445e27b42508 (diff) | |
Add Nexenta volume driver.
Covers blueprint nexenta-volume-driver.
Change-Id: Iac30886981355f99e450a7ffbca24e7c23e4e97d
| -rw-r--r-- | nova/tests/test_nexenta.py | 281 | ||||
| -rw-r--r-- | nova/volume/nexenta/__init__.py | 33 | ||||
| -rw-r--r-- | nova/volume/nexenta/jsonrpc.py | 84 | ||||
| -rw-r--r-- | nova/volume/nexenta/volume.py | 282 |
4 files changed, 680 insertions, 0 deletions
diff --git a/nova/tests/test_nexenta.py b/nova/tests/test_nexenta.py new file mode 100644 index 000000000..1f553fbeb --- /dev/null +++ b/nova/tests/test_nexenta.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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. +""" +Unit tests for OpenStack Nova volume driver +""" + +import base64 +import urllib2 + +import nova.flags +import nova.test +from nova.volume import nexenta +from nova.volume.nexenta import volume +from nova.volume.nexenta import jsonrpc + +FLAGS = nova.flags.FLAGS + + +class TestNexentaDriver(nova.test.TestCase): + TEST_VOLUME_NAME = 'volume1' + TEST_VOLUME_NAME2 = 'volume2' + TEST_SNAPSHOT_NAME = 'snapshot1' + TEST_VOLUME_REF = { + 'name': TEST_VOLUME_NAME, + 'size': 1, + } + TEST_VOLUME_REF2 = { + 'name': TEST_VOLUME_NAME2, + 'size': 1, + } + TEST_SNAPSHOT_REF = { + 'name': TEST_SNAPSHOT_NAME, + 'volume_name': TEST_VOLUME_NAME, + } + + def __init__(self, method): + super(TestNexentaDriver, self).__init__(method) + + def setUp(self): + super(TestNexentaDriver, self).setUp() + self.flags( + nexenta_host='1.1.1.1', + nexenta_volume='nova', + nexenta_target_prefix='iqn:', + nexenta_target_group_prefix='nova/', + nexenta_blocksize='8K', + nexenta_sparse=True, + ) + self.nms_mock = self.mox.CreateMockAnything() + for mod in ['volume', 'zvol', 'iscsitarget', + 'stmf', 'scsidisk', 'snapshot']: + setattr(self.nms_mock, mod, self.mox.CreateMockAnything()) + self.stubs.Set(jsonrpc, 'NexentaJSONProxy', + lambda *_, **__: self.nms_mock) + self.drv = volume.NexentaDriver() + self.drv.do_setup({}) + + def test_setup_error(self): + self.nms_mock.volume.object_exists('nova').AndReturn(True) + self.mox.ReplayAll() + self.drv.check_for_setup_error() + + def test_setup_error_fail(self): + self.nms_mock.volume.object_exists('nova').AndReturn(False) + self.mox.ReplayAll() + self.assertRaises(LookupError, self.drv.check_for_setup_error) + + def test_local_path(self): + self.assertRaises(NotImplementedError, self.drv.local_path, '') + + def test_create_volume(self): + self.nms_mock.zvol.create('nova/volume1', '1G', '8K', True) + self.mox.ReplayAll() + self.drv.create_volume(self.TEST_VOLUME_REF) + + def test_delete_volume(self): + self.nms_mock.zvol.destroy('nova/volume1', '') + self.mox.ReplayAll() + self.drv.delete_volume(self.TEST_VOLUME_REF) + + def test_create_snapshot(self): + self.nms_mock.zvol.create_snapshot('nova/volume1', 'snapshot1', '') + self.mox.ReplayAll() + self.drv.create_snapshot(self.TEST_SNAPSHOT_REF) + + def test_create_volume_from_snapshot(self): + self.nms_mock.zvol.clone('nova/volume1@snapshot1', 'nova/volume2') + self.mox.ReplayAll() + self.drv.create_volume_from_snapshot(self.TEST_VOLUME_REF2, + self.TEST_SNAPSHOT_REF) + + def test_delete_snapshot(self): + self.nms_mock.snapshot.destroy('nova/volume1@snapshot1', '') + self.mox.ReplayAll() + self.drv.delete_snapshot(self.TEST_SNAPSHOT_REF) + + _CREATE_EXPORT_METHODS = [ + ('iscsitarget', 'create_target', ({'target_name': 'iqn:volume1'},), + u'Unable to create iscsi target\n' + u' iSCSI target iqn.1986-03.com.sun:02:nova-volume1 already' + u' configured\n' + u' itadm create-target failed with error 17\n', + ), + ('stmf', 'create_targetgroup', ('nova/volume1',), + u'Unable to create targetgroup: stmfadm: nova/volume1:' + u' already exists\n', + ), + ('stmf', 'add_targetgroup_member', ('nova/volume1', 'iqn:volume1'), + u'Unable to add member to targetgroup: stmfadm:' + u' iqn.1986-03.com.sun:02:nova-volume1: already exists\n', + ), + ('scsidisk', 'create_lu', ('nova/volume1', {}), + u"Unable to create lu with zvol 'nova/volume1':\n" + u" sbdadm: filename /dev/zvol/rdsk/nova/volume1: in use\n", + ), + ('scsidisk', 'add_lun_mapping_entry', ('nova/volume1', { + 'target_group': 'nova/volume1', 'lun': '0'}), + u"Unable to add view to zvol 'nova/volume1' (LUNs in use: ):\n" + u" stmfadm: view entry exists\n", + ), + ] + + def _stub_export_method(self, module, method, args, error, fail=False): + m = getattr(self.nms_mock, module) + m = getattr(m, method) + mock = m(*args) + if fail: + mock.AndRaise(nexenta.NexentaException(error)) + + def _stub_all_export_methods(self, fail=False): + for params in self._CREATE_EXPORT_METHODS: + self._stub_export_method(*params, fail=fail) + + def test_create_export(self): + self._stub_all_export_methods() + self.mox.ReplayAll() + retval = self.drv.create_export({}, self.TEST_VOLUME_REF) + self.assertEquals(retval, + {'provider_location': + '%s:%s,1 %s%s' % (FLAGS.nexenta_host, + FLAGS.nexenta_iscsi_target_portal_port, + FLAGS.nexenta_target_prefix, + self.TEST_VOLUME_NAME)}) + + def __get_test(i): + def _test_create_export_fail(self): + for params in self._CREATE_EXPORT_METHODS[:i]: + self._stub_export_method(*params) + self._stub_export_method(*self._CREATE_EXPORT_METHODS[i], + fail=True) + self.mox.ReplayAll() + self.assertRaises(nexenta.NexentaException, + self.drv.create_export, {}, self.TEST_VOLUME_REF) + return _test_create_export_fail + + for i in range(len(_CREATE_EXPORT_METHODS)): + locals()['test_create_export_fail_%d' % i] = __get_test(i) + + def test_ensure_export(self): + self._stub_all_export_methods(fail=True) + self.mox.ReplayAll() + self.drv.ensure_export({}, self.TEST_VOLUME_REF) + + def test_remove_export(self): + self.nms_mock.scsidisk.delete_lu('nova/volume1') + self.nms_mock.stmf.destroy_targetgroup('nova/volume1') + self.nms_mock.iscsitarget.delete_target('iqn:volume1') + self.mox.ReplayAll() + self.drv.remove_export({}, self.TEST_VOLUME_REF) + + def test_remove_export_fail_0(self): + self.nms_mock.scsidisk.delete_lu('nova/volume1') + self.nms_mock.stmf.destroy_targetgroup('nova/volume1').AndRaise( + nexenta.NexentaException()) + self.nms_mock.iscsitarget.delete_target('iqn:volume1') + self.mox.ReplayAll() + self.drv.remove_export({}, self.TEST_VOLUME_REF) + + def test_remove_export_fail_1(self): + self.nms_mock.scsidisk.delete_lu('nova/volume1') + self.nms_mock.stmf.destroy_targetgroup('nova/volume1') + self.nms_mock.iscsitarget.delete_target('iqn:volume1').AndRaise( + nexenta.NexentaException()) + self.mox.ReplayAll() + self.drv.remove_export({}, self.TEST_VOLUME_REF) + + +class TestNexentaJSONRPC(nova.test.TestCase): + URL = 'http://example.com/' + URL_S = 'https://example.com/' + USER = 'user' + PASSWORD = 'password' + HEADERS = {'Authorization': 'Basic %s' % (base64.b64encode( + ':'.join((USER, PASSWORD))),), + 'Content-Type': 'application/json'} + REQUEST = 'the request' + + def setUp(self): + super(TestNexentaJSONRPC, self).setUp() + self.proxy = jsonrpc.NexentaJSONProxy( + self.URL, self.USER, self.PASSWORD, auto=True) + self.mox.StubOutWithMock(urllib2, 'Request', True) + self.mox.StubOutWithMock(urllib2, 'urlopen') + self.resp_mock = self.mox.CreateMockAnything() + self.resp_info_mock = self.mox.CreateMockAnything() + self.resp_mock.info().AndReturn(self.resp_info_mock) + urllib2.urlopen(self.REQUEST).AndReturn(self.resp_mock) + + def test_call(self): + urllib2.Request(self.URL, + '{"object": null, "params": ["arg1", "arg2"], "method": null}', + self.HEADERS).AndReturn(self.REQUEST) + self.resp_info_mock.status = '' + self.resp_mock.read().AndReturn( + '{"error": null, "result": "the result"}') + self.mox.ReplayAll() + result = self.proxy('arg1', 'arg2') + self.assertEquals("the result", result) + + def test_call_deep(self): + urllib2.Request(self.URL, + '{"object": "obj1.subobj", "params": ["arg1", "arg2"],' + ' "method": "meth"}', + self.HEADERS).AndReturn(self.REQUEST) + self.resp_info_mock.status = '' + self.resp_mock.read().AndReturn( + '{"error": null, "result": "the result"}') + self.mox.ReplayAll() + result = self.proxy.obj1.subobj.meth('arg1', 'arg2') + self.assertEquals("the result", result) + + def test_call_auto(self): + urllib2.Request(self.URL, + '{"object": null, "params": ["arg1", "arg2"], "method": null}', + self.HEADERS).AndReturn(self.REQUEST) + urllib2.Request(self.URL_S, + '{"object": null, "params": ["arg1", "arg2"], "method": null}', + self.HEADERS).AndReturn(self.REQUEST) + self.resp_info_mock.status = 'EOF in headers' + self.resp_mock.read().AndReturn( + '{"error": null, "result": "the result"}') + urllib2.urlopen(self.REQUEST).AndReturn(self.resp_mock) + self.mox.ReplayAll() + result = self.proxy('arg1', 'arg2') + self.assertEquals("the result", result) + + def test_call_error(self): + urllib2.Request(self.URL, + '{"object": null, "params": ["arg1", "arg2"], "method": null}', + self.HEADERS).AndReturn(self.REQUEST) + self.resp_info_mock.status = '' + self.resp_mock.read().AndReturn( + '{"error": {"message": "the error"}, "result": "the result"}') + self.mox.ReplayAll() + self.assertRaises(jsonrpc.NexentaJSONException, + self.proxy, 'arg1', 'arg2') + + def test_call_fail(self): + urllib2.Request(self.URL, + '{"object": null, "params": ["arg1", "arg2"], "method": null}', + self.HEADERS).AndReturn(self.REQUEST) + self.resp_info_mock.status = 'EOF in headers' + self.proxy.auto = False + self.mox.ReplayAll() + self.assertRaises(jsonrpc.NexentaJSONException, + self.proxy, 'arg1', 'arg2') diff --git a/nova/volume/nexenta/__init__.py b/nova/volume/nexenta/__init__.py new file mode 100644 index 000000000..3050df8f6 --- /dev/null +++ b/nova/volume/nexenta/__init__.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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` -- Package contains Nexenta-specific modules +===================================================================== + +.. automodule:: nexenta +.. moduleauthor:: Yuriy Taraday <yorik.sar@gmail.com> +""" + + +class NexentaException(Exception): + MESSAGE = _('Nexenta SA returned the error') + + def __init__(self, error=None): + super(NexentaException, self).__init__(self.message, error) + + def __str__(self): + return '%s: %s' % self.args diff --git a/nova/volume/nexenta/jsonrpc.py b/nova/volume/nexenta/jsonrpc.py new file mode 100644 index 000000000..6487333ec --- /dev/null +++ b/nova/volume/nexenta/jsonrpc.py @@ -0,0 +1,84 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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 +.. moduleauthor:: Yuriy Taraday <yorik.sar@gmail.com> +""" + +import json +import urllib2 + +from nova.volume import nexenta +from nova import log as logging + +LOG = logging.getLogger("nova.volume.nexenta.jsonrpc") + + +class NexentaJSONException(nexenta.NexentaException): + pass + + +class NexentaJSONProxy(object): + def __init__(self, url, user, password, auto=False, obj=None, method=None): + self.url = url + self.user = user + self.password = password + self.auto = auto + self.obj = obj + self.method = method + + def __getattr__(self, name): + if not self.obj: + obj, method = name, None + elif not self.method: + obj, method = self.obj, name + else: + obj, method = '%s.%s' % (self.obj, self.method), name + return NexentaJSONProxy(self.url, self.user, self.password, self.auto, + obj, method) + + def __call__(self, *args): + data = json.dumps({'object': self.obj, + 'method': self.method, + 'params': args}) + auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1] + headers = {'Content-Type': 'application/json', + 'Authorization': 'Basic %s' % (auth,)} + LOG.debug(_('Sending JSON data: %s'), data) + request = urllib2.Request(self.url, data, headers) + response_obj = urllib2.urlopen(request) + if response_obj.info().status == 'EOF in headers': + if self.auto and self.url.startswith('http://'): + LOG.info(_('Auto switching to HTTPS connection to %s'), + self.url) + self.url = 'https' + self.url[4:] + request = urllib2.Request(self.url, data, headers) + response_obj = urllib2.urlopen(request) + else: + LOG.error(_('No headers in server response')) + raise NexentaJSONException(_('Bad response from server')) + + response_data = response_obj.read() + LOG.debug(_('Got response: %s'), response_data) + response = json.loads(response_data) + if response.get('error') is not None: + raise NexentaJSONException(response['error'].get('message', '')) + else: + return response.get('result') diff --git a/nova/volume/nexenta/volume.py b/nova/volume/nexenta/volume.py new file mode 100644 index 000000000..532698ba7 --- /dev/null +++ b/nova/volume/nexenta/volume.py @@ -0,0 +1,282 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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.volume` -- Driver to store volumes on Nexenta Appliance +===================================================================== + +.. automodule:: nexenta.volume +.. moduleauthor:: Yuriy Taraday <yorik.sar@gmail.com> +""" + +from nova import exception +from nova import flags +from nova import log as logging +from nova.openstack.common import cfg +from nova.volume import driver +from nova.volume import nexenta +from nova.volume.nexenta import jsonrpc + +LOG = logging.getLogger("nova.volume.nexenta.volume") +FLAGS = flags.FLAGS + +nexenta_opts = [ + cfg.StrOpt('nexenta_host', + default='', + help='IP address of Nexenta SA'), + cfg.IntOpt('nexenta_rest_port', + default=2000, + help='HTTP port to connect to Nexenta REST API server'), + cfg.StrOpt('nexenta_rest_protocol', + default='auto', + help='Use http or https for REST connection (default auto)'), + cfg.StrOpt('nexenta_user', + default='admin', + help='User name to connect to Nexenta SA'), + cfg.StrOpt('nexenta_password', + default='nexenta', + help='Password to connect to Nexenta SA'), + cfg.IntOpt('nexenta_iscsi_target_portal_port', + default=3260, + help='Nexenta target portal port'), + cfg.StrOpt('nexenta_volume', + default='nova', + help='pool on SA that will hold all volumes'), + cfg.StrOpt('nexenta_target_prefix', + default='iqn.1986-03.com.sun:02:nova-', + help='IQN prefix for iSCSI targets'), + cfg.StrOpt('nexenta_target_group_prefix', + default='nova/', + help='prefix for iSCSI target groups on SA'), + cfg.StrOpt('nexenta_blocksize', + default='', + help='block size for volumes (blank=default,8KB)'), + cfg.BoolOpt('nexenta_sparse', + default=False, + help='flag to create sparse volumes'), +] +FLAGS.register_opts(nexenta_opts) + + +class NexentaDriver(driver.ISCSIDriver): # pylint: disable=R0921 + """Executes volume driver commands on Nexenta Appliance.""" + + def __init__(self): + super(NexentaDriver, self).__init__() + + def do_setup(self, context): + protocol = FLAGS.nexenta_rest_protocol + auto = protocol == 'auto' + if auto: + protocol = 'http' + self.nms = jsonrpc.NexentaJSONProxy( + '%s://%s:%s/rest/nms/' % (protocol, FLAGS.nexenta_host, + FLAGS.nexenta_rest_port), + FLAGS.nexenta_user, FLAGS.nexenta_password, auto=auto) + + def check_for_setup_error(self): + """Verify that the volume for our zvols exists. + + :raise: :py:exc:`LookupError` + """ + if not self.nms.volume.object_exists(FLAGS.nexenta_volume): + raise LookupError(_("Volume %s does not exist in Nexenta SA"), + FLAGS.nexenta_volume) + + @staticmethod + def _get_zvol_name(volume_name): + """Return zvol name that corresponds given volume name.""" + return '%s/%s' % (FLAGS.nexenta_volume, volume_name) + + @staticmethod + def _get_target_name(volume_name): + """Return iSCSI target name to access volume.""" + return '%s%s' % (FLAGS.nexenta_target_prefix, volume_name) + + @staticmethod + def _get_target_group_name(volume_name): + """Return Nexenta iSCSI target group name for volume.""" + return '%s%s' % (FLAGS.nexenta_target_group_prefix, volume_name) + + def create_volume(self, volume): + """Create a zvol on appliance. + + :param volume: volume reference + """ + self.nms.zvol.create( + self._get_zvol_name(volume['name']), + '%sG' % (volume['size'],), + FLAGS.nexenta_blocksize, FLAGS.nexenta_sparse) + + def delete_volume(self, volume): + """Destroy a zvol on appliance. + + :param volume: volume reference + """ + try: + self.nms.zvol.destroy(self._get_zvol_name(volume['name']), '') + except nexenta.NexentaException as exc: + if "zvol has children" in exc.args[1]: + raise exception.VolumeIsBusy + else: + raise + + def create_snapshot(self, snapshot): + """Create snapshot of existing zvol on appliance. + + :param snapshot: shapshot reference + """ + self.nms.zvol.create_snapshot( + self._get_zvol_name(snapshot['volume_name']), + snapshot['name'], '') + + 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 + """ + self.nms.zvol.clone( + '%s@%s' % (self._get_zvol_name(snapshot['volume_name']), + snapshot['name']), + self._get_zvol_name(volume['name'])) + + def delete_snapshot(self, snapshot): + """Delete volume's snapshot on appliance. + + :param snapshot: shapshot reference + """ + try: + self.nms.snapshot.destroy( + '%s@%s' % (self._get_zvol_name(snapshot['volume_name']), + snapshot['name']), + '') + except nexenta.NexentaException as exc: + if "snapshot has dependent clones" in exc.args[1]: + raise exception.SnapshotIsBusy + else: + raise + + def local_path(self, volume): + """Return local path to existing local volume. + + We never have local volumes, so it raises NotImplementedError. + + :raise: :py:exc:`NotImplementedError` + """ + LOG.error(_("Call to local_path should not happen." + " Verify that use_local_volumes flag is turned off.")) + raise NotImplementedError + + def _do_export(self, _ctx, volume, ensure=False): + """Do all steps to get zvol exported as LUN 0 at separate target. + + :param volume: reference of volume to be exported + :param ensure: if True, ignore errors caused by already existing + resources + :return: iscsiadm-formatted provider location string + """ + zvol_name = self._get_zvol_name(volume['name']) + target_name = self._get_target_name(volume['name']) + target_group_name = self._get_target_group_name(volume['name']) + + try: + self.nms.iscsitarget.create_target({'target_name': target_name}) + except nexenta.NexentaException as exc: + if not ensure or 'already configured' not in exc.args[1]: + raise + else: + LOG.info(_('Ignored target creation error "%s"' + ' while ensuring export'), exc) + try: + self.nms.stmf.create_targetgroup(target_group_name) + except nexenta.NexentaException as exc: + if not ensure or 'already exists' not in exc.args[1]: + raise + else: + LOG.info(_('Ignored target group creation error "%s"' + ' while ensuring export'), exc) + try: + self.nms.stmf.add_targetgroup_member(target_group_name, + target_name) + except nexenta.NexentaException as exc: + if not ensure or 'already exists' not in exc.args[1]: + raise + else: + LOG.info(_('Ignored target group member addition error "%s"' + ' while ensuring export'), exc) + try: + self.nms.scsidisk.create_lu(zvol_name, {}) + except nexenta.NexentaException as exc: + if not ensure or 'in use' not in exc.args[1]: + raise + else: + LOG.info(_('Ignored LU creation error "%s"' + ' while ensuring export'), exc) + try: + self.nms.scsidisk.add_lun_mapping_entry(zvol_name, { + 'target_group': target_group_name, + 'lun': '0'}) + except nexenta.NexentaException as exc: + if not ensure or 'view entry exists' not in exc.args[1]: + raise + else: + LOG.info(_('Ignored LUN mapping entry addition error "%s"' + ' while ensuring export'), exc) + return '%s:%s,1 %s' % (FLAGS.nexenta_host, + FLAGS.nexenta_iscsi_target_portal_port, + target_name) + + def create_export(self, _ctx, volume): + """Create new export for zvol. + + :param volume: reference of volume to be exported + :return: iscsiadm-formatted provider location string + """ + loc = self._do_export(_ctx, volume, ensure=False) + return {'provider_location': loc} + + 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, ensure=True) + + def remove_export(self, _ctx, volume): + """Destroy all resources created to export zvol. + + :param volume: reference of volume to be unexported + """ + zvol_name = self._get_zvol_name(volume['name']) + target_name = self._get_target_name(volume['name']) + target_group_name = self._get_target_group_name(volume['name']) + self.nms.scsidisk.delete_lu(zvol_name) + + try: + self.nms.stmf.destroy_targetgroup(target_group_name) + except nexenta.NexentaException as exc: + # We assume that target group is already gone + LOG.warn(_('Got error trying to destroy target group' + ' %(target_group)s, assuming it is already gone: %(exc)s'), + {'target_group': target_group_name, 'exc': exc}) + try: + self.nms.iscsitarget.delete_target(target_name) + except nexenta.NexentaException as exc: + # We assume that target is gone as well + LOG.warn(_('Got error trying to delete target %(target)s,' + ' assuming it is already gone: %(exc)s'), + {'target': target_name, 'exc': exc}) |
