Add Nexenta volume driver.
Covers blueprint nexenta-volume-driver. Change-Id: Iac30886981355f99e450a7ffbca24e7c23e4e97d
This commit is contained in:
parent
160e6b6eee
commit
4ba4fb6a4e
281
nova/tests/test_nexenta.py
Normal file
281
nova/tests/test_nexenta.py
Normal file
@ -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')
|
33
nova/volume/nexenta/__init__.py
Normal file
33
nova/volume/nexenta/__init__.py
Normal file
@ -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
|
84
nova/volume/nexenta/jsonrpc.py
Normal file
84
nova/volume/nexenta/jsonrpc.py
Normal file
@ -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')
|
282
nova/volume/nexenta/volume.py
Normal file
282
nova/volume/nexenta/volume.py
Normal file
@ -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})
|
Loading…
x
Reference in New Issue
Block a user