summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYuriy Taraday <yorik.sar@gmail.com>2012-02-15 21:31:51 +0400
committerYuriy Taraday <yorik.sar@gmail.com>2012-02-23 00:43:27 +0400
commit4ba4fb6a4e09e4be9a17f6da78820834a6676b92 (patch)
treeaaa705c38334c3b4fe82920949cf31be129c3da7
parent160e6b6eee05f4273dc52770575d445e27b42508 (diff)
Add Nexenta volume driver.
Covers blueprint nexenta-volume-driver. Change-Id: Iac30886981355f99e450a7ffbca24e7c23e4e97d
-rw-r--r--nova/tests/test_nexenta.py281
-rw-r--r--nova/volume/nexenta/__init__.py33
-rw-r--r--nova/volume/nexenta/jsonrpc.py84
-rw-r--r--nova/volume/nexenta/volume.py282
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})