Nexenta Edge iSCSI backend driver
Nexenta Edge project supports iSCSI block level storage. This patch implements a driver for Nexenta Edge iSCSI backend. DocImpact Implements: blueprint nexentaedge-iscsi-volume-driver Change-Id: I82c215ba85e9d49723e792d88d86553b3a75d3ac
This commit is contained in:
parent
cdc645e2f9
commit
ee352dcf03
@ -119,6 +119,8 @@ from cinder.volume.drivers.lenovo import lenovo_common as \
|
|||||||
from cinder.volume.drivers import lvm as cinder_volume_drivers_lvm
|
from cinder.volume.drivers import lvm as cinder_volume_drivers_lvm
|
||||||
from cinder.volume.drivers.netapp import options as \
|
from cinder.volume.drivers.netapp import options as \
|
||||||
cinder_volume_drivers_netapp_options
|
cinder_volume_drivers_netapp_options
|
||||||
|
from cinder.volume.drivers.nexenta.nexentaedge import iscsi as \
|
||||||
|
cinder_volume_drivers_nexenta_nexentaedge_iscsi
|
||||||
from cinder.volume.drivers import nfs as cinder_volume_drivers_nfs
|
from cinder.volume.drivers import nfs as cinder_volume_drivers_nfs
|
||||||
from cinder.volume.drivers import nimble as cinder_volume_drivers_nimble
|
from cinder.volume.drivers import nimble as cinder_volume_drivers_nimble
|
||||||
from cinder.volume.drivers.prophetstor import options as \
|
from cinder.volume.drivers.prophetstor import options as \
|
||||||
@ -287,6 +289,8 @@ def list_opts():
|
|||||||
cinder_volume_drivers_prophetstor_options.DPL_OPTS,
|
cinder_volume_drivers_prophetstor_options.DPL_OPTS,
|
||||||
cinder_volume_drivers_hitachi_hbsdiscsi.volume_opts,
|
cinder_volume_drivers_hitachi_hbsdiscsi.volume_opts,
|
||||||
cinder_volume_manager.volume_manager_opts,
|
cinder_volume_manager.volume_manager_opts,
|
||||||
|
cinder_volume_drivers_nexenta_nexentaedge_iscsi.
|
||||||
|
nexenta_edge_opts,
|
||||||
cinder_volume_drivers_ibm_flashsystemiscsi.
|
cinder_volume_drivers_ibm_flashsystemiscsi.
|
||||||
flashsystem_iscsi_opts,
|
flashsystem_iscsi_opts,
|
||||||
cinder_volume_drivers_ibm_flashsystemcommon.flashsystem_opts,
|
cinder_volume_drivers_ibm_flashsystemcommon.flashsystem_opts,
|
||||||
|
169
cinder/tests/unit/test_nexenta_edge.py
Normal file
169
cinder/tests/unit/test_nexenta_edge.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2015 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
|
from cinder import test
|
||||||
|
from cinder.volume import configuration as conf
|
||||||
|
from cinder.volume.drivers.nexenta.nexentaedge import iscsi
|
||||||
|
|
||||||
|
NEDGE_URL = 'service/isc/iscsi'
|
||||||
|
NEDGE_BUCKET = 'c/t/bk'
|
||||||
|
NEDGE_SERVICE = 'isc'
|
||||||
|
NEDGE_BLOCKSIZE = 4096
|
||||||
|
NEDGE_CHUNKSIZE = 16384
|
||||||
|
|
||||||
|
MOCK_VOL = {
|
||||||
|
'id': 'vol1',
|
||||||
|
'name': 'vol1',
|
||||||
|
'size': 1
|
||||||
|
}
|
||||||
|
MOCK_VOL2 = {
|
||||||
|
'id': 'vol2',
|
||||||
|
'name': 'vol2',
|
||||||
|
'size': 1
|
||||||
|
}
|
||||||
|
MOCK_SNAP = {
|
||||||
|
'id': 'snap1',
|
||||||
|
'name': 'snap1',
|
||||||
|
'volume_name': 'vol1'
|
||||||
|
}
|
||||||
|
NEW_VOL_SIZE = 2
|
||||||
|
ISCSI_TARGET_NAME = 'iscsi_target_name'
|
||||||
|
ISCSI_TARGET_STATUS = 'Target 1: ' + ISCSI_TARGET_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class TestNexentaEdgeISCSIDriver(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNexentaEdgeISCSIDriver, self).setUp()
|
||||||
|
self.cfg = mock.Mock(spec=conf.Configuration)
|
||||||
|
self.cfg.nexenta_client_address = '0.0.0.0'
|
||||||
|
self.cfg.nexenta_rest_address = '0.0.0.0'
|
||||||
|
self.cfg.nexenta_rest_port = 8080
|
||||||
|
self.cfg.nexenta_rest_protocol = 'http'
|
||||||
|
self.cfg.nexenta_iscsi_target_portal_port = 3260
|
||||||
|
self.cfg.nexenta_rest_user = 'admin'
|
||||||
|
self.cfg.nexenta_rest_password = 'admin'
|
||||||
|
self.cfg.nexenta_lun_container = NEDGE_BUCKET
|
||||||
|
self.cfg.nexenta_iscsi_service = NEDGE_SERVICE
|
||||||
|
self.cfg.nexenta_blocksize = NEDGE_BLOCKSIZE
|
||||||
|
self.cfg.nexenta_chunksize = NEDGE_CHUNKSIZE
|
||||||
|
|
||||||
|
mock_exec = mock.Mock()
|
||||||
|
mock_exec.return_value = ('', '')
|
||||||
|
self.driver = iscsi.NexentaEdgeISCSIDriver(execute=mock_exec,
|
||||||
|
configuration=self.cfg)
|
||||||
|
self.api_patcher = mock.patch('cinder.volume.drivers.nexenta.'
|
||||||
|
'nexentaedge.jsonrpc.'
|
||||||
|
'NexentaEdgeJSONProxy.__call__')
|
||||||
|
self.mock_api = self.api_patcher.start()
|
||||||
|
|
||||||
|
self.mock_api.return_value = {
|
||||||
|
'data': {'value': ISCSI_TARGET_STATUS}
|
||||||
|
}
|
||||||
|
self.driver.do_setup(context.get_admin_context())
|
||||||
|
|
||||||
|
self.addCleanup(self.api_patcher.stop)
|
||||||
|
|
||||||
|
def test_check_do_setup(self):
|
||||||
|
self.assertEqual(ISCSI_TARGET_NAME, self.driver.target_name)
|
||||||
|
|
||||||
|
def test_create_volume(self):
|
||||||
|
self.driver.create_volume(MOCK_VOL)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||||
|
'volSizeMB': MOCK_VOL['size'] * 1024,
|
||||||
|
'blockSize': NEDGE_BLOCKSIZE,
|
||||||
|
'chunkSize': NEDGE_CHUNKSIZE
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_create_volume_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL)
|
||||||
|
|
||||||
|
def test_delete_volume(self):
|
||||||
|
self.driver.delete_volume(MOCK_VOL)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id']
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_delete_volume_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError, self.driver.delete_volume, MOCK_VOL)
|
||||||
|
|
||||||
|
def test_extend_volume(self):
|
||||||
|
self.driver.extend_volume(MOCK_VOL, NEW_VOL_SIZE)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL + '/resize', {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||||
|
'newSizeMB': NEW_VOL_SIZE * 1024
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_extend_volume_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError, self.driver.extend_volume,
|
||||||
|
MOCK_VOL, NEW_VOL_SIZE)
|
||||||
|
|
||||||
|
def test_create_snapshot(self):
|
||||||
|
self.driver.create_snapshot(MOCK_SNAP)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||||
|
'snapName': MOCK_SNAP['id']
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_create_snapshot_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError, self.driver.create_snapshot, MOCK_SNAP)
|
||||||
|
|
||||||
|
def test_delete_snapshot(self):
|
||||||
|
self.driver.delete_snapshot(MOCK_SNAP)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||||
|
'snapName': MOCK_SNAP['id']
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_delete_snapshot_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError, self.driver.delete_snapshot, MOCK_SNAP)
|
||||||
|
|
||||||
|
def test_create_volume_from_snapshot(self):
|
||||||
|
self.driver.create_volume_from_snapshot(MOCK_VOL2, MOCK_SNAP)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot/clone', {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_SNAP['volume_name'],
|
||||||
|
'clonePath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
|
||||||
|
'snapName': MOCK_SNAP['id']
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_create_volume_from_snapshot_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError,
|
||||||
|
self.driver.create_volume_from_snapshot,
|
||||||
|
MOCK_VOL2, MOCK_SNAP)
|
||||||
|
|
||||||
|
def test_create_cloned_volume(self):
|
||||||
|
self.driver.create_cloned_volume(MOCK_VOL2, MOCK_VOL)
|
||||||
|
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||||
|
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
|
||||||
|
'volSizeMB': MOCK_VOL2['size'] * 1024,
|
||||||
|
'blockSize': NEDGE_BLOCKSIZE,
|
||||||
|
'chunkSize': NEDGE_CHUNKSIZE
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_create_cloned_volume_fail(self):
|
||||||
|
self.mock_api.side_effect = RuntimeError
|
||||||
|
self.assertRaises(RuntimeError, self.driver.create_cloned_volume,
|
||||||
|
MOCK_VOL2, MOCK_VOL)
|
0
cinder/volume/drivers/nexenta/__init__.py
Normal file
0
cinder/volume/drivers/nexenta/__init__.py
Normal file
303
cinder/volume/drivers/nexenta/nexentaedge/iscsi.py
Normal file
303
cinder/volume/drivers/nexenta/nexentaedge/iscsi.py
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import units
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _, _LE
|
||||||
|
from cinder.volume import driver
|
||||||
|
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
|
||||||
|
|
||||||
|
|
||||||
|
nexenta_edge_opts = [
|
||||||
|
cfg.StrOpt('nexenta_rest_address',
|
||||||
|
default='',
|
||||||
|
help='IP address of NexentaEdge management REST API endpoint'),
|
||||||
|
cfg.IntOpt('nexenta_rest_port',
|
||||||
|
default=8080,
|
||||||
|
help='HTTP port to connect to NexentaEdge REST API endpoint'),
|
||||||
|
cfg.StrOpt('nexenta_rest_protocol',
|
||||||
|
default='auto',
|
||||||
|
help='Use http or https for REST connection (default auto)'),
|
||||||
|
cfg.IntOpt('nexenta_iscsi_target_portal_port',
|
||||||
|
default=3260,
|
||||||
|
help='NexentaEdge target portal port'),
|
||||||
|
cfg.StrOpt('nexenta_rest_user',
|
||||||
|
default='admin',
|
||||||
|
help='User name to connect to NexentaEdge'),
|
||||||
|
cfg.StrOpt('nexenta_rest_password',
|
||||||
|
default='nexenta',
|
||||||
|
help='Password to connect to NexentaEdge',
|
||||||
|
secret=True),
|
||||||
|
cfg.StrOpt('nexenta_lun_container',
|
||||||
|
default='',
|
||||||
|
help='NexentaEdge logical path of bucket for LUNs'),
|
||||||
|
cfg.StrOpt('nexenta_iscsi_service',
|
||||||
|
default='',
|
||||||
|
help='NexentaEdge iSCSI service name'),
|
||||||
|
cfg.StrOpt('nexenta_client_address',
|
||||||
|
default='',
|
||||||
|
help='NexentaEdge iSCSI Gateway client '
|
||||||
|
'address for non-VIP service'),
|
||||||
|
cfg.StrOpt('nexenta_blocksize',
|
||||||
|
default=4096,
|
||||||
|
help='NexentaEdge iSCSI LUN block size'),
|
||||||
|
cfg.StrOpt('nexenta_chunksize',
|
||||||
|
default=16384,
|
||||||
|
help='NexentaEdge iSCSI LUN object chunk size')
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(nexenta_edge_opts)
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
|
||||||
|
"""Executes volume driver commands on NexentaEdge cluster.
|
||||||
|
|
||||||
|
Version history:
|
||||||
|
1.0.0 - Initial driver version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
VERSION = '1.0.0'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs)
|
||||||
|
if self.configuration:
|
||||||
|
self.configuration.append_config_values(nexenta_edge_opts)
|
||||||
|
self.restapi_protocol = self.configuration.nexenta_rest_protocol
|
||||||
|
self.restapi_host = self.configuration.nexenta_rest_address
|
||||||
|
self.restapi_port = self.configuration.nexenta_rest_port
|
||||||
|
self.restapi_user = self.configuration.nexenta_rest_user
|
||||||
|
self.restapi_password = self.configuration.nexenta_rest_password
|
||||||
|
self.iscsi_service = self.configuration.nexenta_iscsi_service
|
||||||
|
self.bucket_path = self.configuration.nexenta_lun_container
|
||||||
|
self.blocksize = self.configuration.nexenta_blocksize
|
||||||
|
self.chunksize = self.configuration.nexenta_chunksize
|
||||||
|
self.cluster, self.tenant, self.bucket = self.bucket_path.split('/')
|
||||||
|
self.bucket_url = ('clusters/' + self.cluster + '/tenants/' +
|
||||||
|
self.tenant + '/buckets/' + self.bucket)
|
||||||
|
self.iscsi_target_port = (self.configuration.
|
||||||
|
nexenta_iscsi_target_portal_port)
|
||||||
|
self.target_vip = None
|
||||||
|
|
||||||
|
@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.restapi_protocol == 'auto':
|
||||||
|
protocol, auto = 'http', True
|
||||||
|
else:
|
||||||
|
protocol, auto = self.restapi_protocol, False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.restapi = jsonrpc.NexentaEdgeJSONProxy(
|
||||||
|
protocol, self.restapi_host, self.restapi_port, '/',
|
||||||
|
self.restapi_user, self.restapi_password, auto=auto)
|
||||||
|
|
||||||
|
rsp = self.restapi.get('service/'
|
||||||
|
+ self.iscsi_service + '/iscsi/status')
|
||||||
|
data_keys = rsp['data'][list(rsp['data'].keys())[0]]
|
||||||
|
self.target_name = data_keys.split('\n', 1)[0].split(' ')[2]
|
||||||
|
|
||||||
|
rsp = self.restapi.get('service/' + self.iscsi_service)
|
||||||
|
if 'X-VIPS' in rsp['data']:
|
||||||
|
vips = json.loads(rsp['data']['X-VIPS'])
|
||||||
|
if len(vips[0]) == 1:
|
||||||
|
self.target_vip = vips[0][0]['ip'].split('/', 1)[0]
|
||||||
|
else:
|
||||||
|
self.target_vip = vips[0][1]['ip'].split('/', 1)[0]
|
||||||
|
else:
|
||||||
|
self.target_vip = self.configuration.safe_get(
|
||||||
|
'nexenta_client_address')
|
||||||
|
if not self.target_vip:
|
||||||
|
LOG.error(_LE('No VIP configured for service %s'),
|
||||||
|
self.iscsi_service)
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
_('No service VIP configured and '
|
||||||
|
'no nexenta_client_address'))
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error verifying iSCSI service %(serv)s on '
|
||||||
|
'host %(hst)s'), {'serv': self.iscsi_service,
|
||||||
|
'hst': self.restapi_host})
|
||||||
|
|
||||||
|
def check_for_setup_error(self):
|
||||||
|
try:
|
||||||
|
self.restapi.get(self.bucket_url + '/objects/')
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error verifying LUN container %(bkt)s'),
|
||||||
|
{'bkt': self.bucket_path})
|
||||||
|
|
||||||
|
def _get_lun_number(self, volname):
|
||||||
|
try:
|
||||||
|
rsp = self.restapi.put(
|
||||||
|
'service/' + self.iscsi_service + '/iscsi/number',
|
||||||
|
{
|
||||||
|
'objectPath': self.bucket_path + '/' + volname
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error retrieving LUN %(vol)s number'),
|
||||||
|
{'vol': volname})
|
||||||
|
|
||||||
|
return rsp['data']
|
||||||
|
|
||||||
|
def _get_target_address(self, volname):
|
||||||
|
return self.target_vip
|
||||||
|
|
||||||
|
def _get_provider_location(self, volume):
|
||||||
|
return '%(host)s:%(port)s,1 %(name)s %(number)s' % {
|
||||||
|
'host': self._get_target_address(volume['name']),
|
||||||
|
'port': self.iscsi_target_port,
|
||||||
|
'name': self.target_name,
|
||||||
|
'number': self._get_lun_number(volume['name'])
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_volume(self, volume):
|
||||||
|
try:
|
||||||
|
self.restapi.post('service/' + self.iscsi_service + '/iscsi', {
|
||||||
|
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'volSizeMB': int(volume['size']) * units.Ki,
|
||||||
|
'blockSize': self.blocksize,
|
||||||
|
'chunkSize': self.chunksize
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating volume'))
|
||||||
|
|
||||||
|
def delete_volume(self, volume):
|
||||||
|
try:
|
||||||
|
self.restapi.delete('service/' + self.iscsi_service +
|
||||||
|
'/iscsi', {'objectPath': self.bucket_path +
|
||||||
|
'/' + volume['name']})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error deleting volume'))
|
||||||
|
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
try:
|
||||||
|
self.restapi.put('service/' + self.iscsi_service + '/iscsi/resize',
|
||||||
|
{'objectPath': self.bucket_path +
|
||||||
|
'/' + volume['name'],
|
||||||
|
'newSizeMB': new_size * units.Ki})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error extending volume'))
|
||||||
|
|
||||||
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
|
try:
|
||||||
|
self.restapi.put(
|
||||||
|
'service/' + self.iscsi_service + '/iscsi/snapshot/clone',
|
||||||
|
{
|
||||||
|
'objectPath': self.bucket_path + '/' +
|
||||||
|
snapshot['volume_name'],
|
||||||
|
'clonePath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'snapName': snapshot['name']
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error cloning volume'))
|
||||||
|
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
try:
|
||||||
|
self.restapi.post(
|
||||||
|
'service/' + self.iscsi_service + '/iscsi/snapshot',
|
||||||
|
{
|
||||||
|
'objectPath': self.bucket_path + '/' +
|
||||||
|
snapshot['volume_name'],
|
||||||
|
'snapName': snapshot['name']
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating snapshot'))
|
||||||
|
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
try:
|
||||||
|
self.restapi.delete(
|
||||||
|
'service/' + self.iscsi_service + '/iscsi/snapshot',
|
||||||
|
{
|
||||||
|
'objectPath': self.bucket_path + '/' +
|
||||||
|
snapshot['volume_name'],
|
||||||
|
'snapName': snapshot['name']
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error deleting snapshot'))
|
||||||
|
|
||||||
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
|
vol_url = (self.bucket_url + '/objects/' +
|
||||||
|
src_vref['name'] + '/clone')
|
||||||
|
clone_body = {
|
||||||
|
'tenant_name': self.tenant,
|
||||||
|
'bucket_name': self.bucket,
|
||||||
|
'object_name': volume['name']
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self.restapi.post(vol_url, clone_body)
|
||||||
|
self.restapi.post('service/' + self.iscsi_service + '/iscsi', {
|
||||||
|
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'volSizeMB': int(src_vref['size']) * units.Ki,
|
||||||
|
'blockSize': self.blocksize,
|
||||||
|
'chunkSize': self.chunksize
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating cloned volume'))
|
||||||
|
|
||||||
|
def create_export(self, context, volume, connector=None):
|
||||||
|
return {'provider_location': self._get_provider_location(volume)}
|
||||||
|
|
||||||
|
def ensure_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def local_path(self, volume):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_volume_stats(self, refresh=False):
|
||||||
|
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||||
|
'driver': self.__class__.__name__,
|
||||||
|
'host': self._get_target_address(None),
|
||||||
|
'bucket': self.bucket_path
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'vendor_name': 'Nexenta',
|
||||||
|
'driver_version': self.VERSION,
|
||||||
|
'storage_protocol': 'iSCSI',
|
||||||
|
'reserved_percentage': 0,
|
||||||
|
'total_capacity_gb': 'unknown',
|
||||||
|
'free_capacity_gb': 'unknown',
|
||||||
|
'QoS_support': False,
|
||||||
|
'volume_backend_name': self.backend_name,
|
||||||
|
'location_info': location_info,
|
||||||
|
'iscsi_target_portal_port': self.iscsi_target_port,
|
||||||
|
'restapi_url': self.restapi.url
|
||||||
|
}
|
100
cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py
Normal file
100
cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Copyright 2015 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
from cinder.utils import retry
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
socket.setdefaulttimeout(100)
|
||||||
|
|
||||||
|
|
||||||
|
class NexentaEdgeJSONProxy(object):
|
||||||
|
|
||||||
|
retry_exc_tuple = (
|
||||||
|
requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.ConnectTimeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, protocol, host, port, path, user, password, auto=False,
|
||||||
|
method=None):
|
||||||
|
self.protocol = protocol.lower()
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.path = path
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.auto = auto
|
||||||
|
self.method = method
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return '%s://%s:%s%s' % (self.protocol,
|
||||||
|
self.host, self.port, self.path)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if not self.method:
|
||||||
|
method = name
|
||||||
|
else:
|
||||||
|
raise exception.VolumeDriverException(
|
||||||
|
_("Wrong resource call syntax"))
|
||||||
|
return NexentaEdgeJSONProxy(
|
||||||
|
self.protocol, self.host, self.port, self.path,
|
||||||
|
self.user, self.password, self.auto, method)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.url.__hash___()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'HTTP JSON proxy: %s' % self.url
|
||||||
|
|
||||||
|
@retry(retry_exc_tuple, interval=1, retries=6)
|
||||||
|
def __call__(self, *args):
|
||||||
|
self.path += args[0]
|
||||||
|
data = None
|
||||||
|
if len(args) > 1:
|
||||||
|
data = json.dumps(args[1])
|
||||||
|
|
||||||
|
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', self.url)
|
||||||
|
|
||||||
|
if self.method == 'get':
|
||||||
|
req = requests.get(self.url, headers=headers)
|
||||||
|
if self.method == 'post':
|
||||||
|
req = requests.post(self.url, data=data, headers=headers)
|
||||||
|
if self.method == 'put':
|
||||||
|
req = requests.put(self.url, data=data, headers=headers)
|
||||||
|
if self.method == 'delete':
|
||||||
|
req = requests.delete(self.url, data=data, headers=headers)
|
||||||
|
|
||||||
|
rsp = req.json()
|
||||||
|
req.close()
|
||||||
|
|
||||||
|
LOG.debug('Got response: %s', rsp)
|
||||||
|
if rsp.get('response') is None:
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
_('Error response: %s') % rsp)
|
||||||
|
return rsp.get('response')
|
@ -92,6 +92,7 @@ cinder.tests.unit.test_misc
|
|||||||
cinder.tests.unit.test_netapp
|
cinder.tests.unit.test_netapp
|
||||||
cinder.tests.unit.test_netapp_nfs
|
cinder.tests.unit.test_netapp_nfs
|
||||||
cinder.tests.unit.test_netapp_ssc
|
cinder.tests.unit.test_netapp_ssc
|
||||||
|
cinder.tests.unit.test_nexenta_edge
|
||||||
cinder.tests.unit.test_nfs
|
cinder.tests.unit.test_nfs
|
||||||
cinder.tests.unit.test_nimble
|
cinder.tests.unit.test_nimble
|
||||||
cinder.tests.unit.test_prophetstor_dpl
|
cinder.tests.unit.test_prophetstor_dpl
|
||||||
|
Loading…
Reference in New Issue
Block a user